From fb1ceebaa481b0ff111a589d6ef47d53e02d9747 Mon Sep 17 00:00:00 2001 From: bdalsass <95754414+bdalsass@users.noreply.github.com> Date: Tue, 24 Jan 2023 10:03:10 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B03190=20-=20Edit=20n:n=20LinkedSetIndirec?= =?UTF-8?q?t=20in=20object=20details=20using=20a=20tagset-like=20widget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add generic set block ui component - Add model link set (direct and indirect) attribute (display style) - Add model link set direct allowed values - Create link set viewer block UI (BlockLinksSetDisplayAsProperty) - Add set block ui factory for linkset - Add object factory and create new endpoint in object controller (with data binder) - Add link set model, link set repository and link set data transformer services --- application/cmdbabstract.class.inc.php | 200 +++++---- application/wizardhelper.class.inc.php | 14 +- core/attributedef.class.inc.php | 130 ++++-- core/ormlinkset.class.inc.php | 23 +- css/backoffice/application/_all.scss | 2 + css/backoffice/application/bulk/_all.scss | 6 + .../application/bulk/_bulk-modify.scss | 14 + .../application/linked-set/_all.scss | 6 + .../linked-set/_linked-set-selector.scss | 14 + css/backoffice/vendors/_selectize.scss | 85 ++++ .../bulk/en.dictionary.itop.bulk.php | 25 ++ .../bulk/fr.dictionary.itop.bulk.php | 25 ++ .../links/en.dictionary.itop.links.php | 38 +- .../links/fr.dictionary.itop.links.php | 26 +- js/selectize/plugin_combodo_add_button.js | 60 +++ js/selectize/plugin_combodo_auto_position.js | 62 +++ .../plugin_combodo_multi_values_synthesis.js | 280 ++++++++++++ .../plugin_combodo_update_operations.js | 112 +++++ js/utils.js | 99 +++++ lib/composer/ClassLoader.php | 2 +- lib/composer/InstalledVersions.php | 13 + lib/composer/autoload_classmap.php | 15 + lib/composer/autoload_real.php | 9 +- lib/composer/autoload_static.php | 15 + lib/composer/installed.php | 4 +- setup/compiler.class.inc.php | 9 + setup/itop_design.xsd | 1 + .../Set/DataProvider/AbstractDataProvider.php | 160 +++++++ .../Set/DataProvider/AjaxDataProvider.php | 169 ++++++++ .../DataProvider/AjaxDataProviderForOql.php | 65 +++ .../Set/DataProvider/SimpleDataProvider.php | 95 +++++ .../Input/Set/DataProvider/iDataProvider.php | 145 +++++++ .../UI/Base/Component/Input/Set/Set.php | 397 ++++++++++++++++++ .../Component/Input/Set/SetUIBlockFactory.php | 154 +++++++ .../UI/Links/AbstractBlockLinksViewTable.php | 76 ++-- .../Direct/BlockDirectLinksEditTable.php | 22 +- .../Indirect/BlockIndirectLinksViewTable.php | 3 +- .../Set/BlockLinksSetDisplayAsProperty.php | 161 +++++++ .../UI/Links/Set/LinksSetUIBlockFactory.php | 118 ++++++ .../Base/Layout/ObjectController.php | 41 ++ .../Controller/Links/LinksetController.php | 7 +- sources/Service/Base/ObjectRepository.php | 263 ++++++++++++ sources/Service/Base/iDataPostProcessor.php | 40 ++ .../Service/Links/LinkSetDataTransformer.php | 176 ++++++++ sources/Service/Links/LinkSetModel.php | 75 ++++ sources/Service/Links/LinkSetRepository.php | 100 +++++ .../Links/LinksBulkDataPostProcessor.php | 139 ++++++ .../object/set/item_renderer.html.twig | 15 + .../object/set/option_renderer.html.twig | 29 ++ .../object/set/set_renderer.html.twig | 13 + .../base/components/input/layout.html.twig | 3 +- .../components/input/set/layout.html.twig | 26 ++ .../components/input/set/layout.ready.js.twig | 206 +++++++++ .../set/simple_option_renderer.html.twig | 15 + .../Backoffice/RenderAllUiBlocks.php | 180 +++++++- 55 files changed, 3948 insertions(+), 234 deletions(-) create mode 100644 css/backoffice/application/bulk/_all.scss create mode 100644 css/backoffice/application/bulk/_bulk-modify.scss create mode 100644 css/backoffice/application/linked-set/_all.scss create mode 100644 css/backoffice/application/linked-set/_linked-set-selector.scss create mode 100644 dictionaries/ui/application/bulk/en.dictionary.itop.bulk.php create mode 100644 dictionaries/ui/application/bulk/fr.dictionary.itop.bulk.php create mode 100644 js/selectize/plugin_combodo_add_button.js create mode 100644 js/selectize/plugin_combodo_auto_position.js create mode 100644 js/selectize/plugin_combodo_multi_values_synthesis.js create mode 100644 js/selectize/plugin_combodo_update_operations.js create mode 100644 sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/Set.php create mode 100644 sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php create mode 100644 sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php create mode 100644 sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php create mode 100644 sources/Service/Base/ObjectRepository.php create mode 100644 sources/Service/Base/iDataPostProcessor.php create mode 100644 sources/Service/Links/LinkSetDataTransformer.php create mode 100644 sources/Service/Links/LinkSetModel.php create mode 100644 sources/Service/Links/LinkSetRepository.php create mode 100644 sources/Service/Links/LinksBulkDataPostProcessor.php create mode 100644 templates/application/object/set/item_renderer.html.twig create mode 100644 templates/application/object/set/option_renderer.html.twig create mode 100644 templates/application/object/set/set_renderer.html.twig create mode 100644 templates/base/components/input/set/layout.html.twig create mode 100644 templates/base/components/input/set/layout.ready.js.twig create mode 100644 templates/base/components/input/set/simple_option_renderer.html.twig diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 238de73aa..ac4698cfe 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -41,8 +41,12 @@ use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory; use Combodo\iTop\Application\UI\Links\Direct\BlockDirectLinksViewTable; use Combodo\iTop\Application\UI\Links\Indirect\BlockIndirectLinksViewTable; +use Combodo\iTop\Application\UI\Links\Set\LinksSetUIBlockFactory; use Combodo\iTop\Renderer\BlockRenderer; +use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer; use Combodo\iTop\Renderer\Console\ConsoleFormRenderer; +use Combodo\iTop\Service\Links\LinkSetDataTransformer; +use Combodo\iTop\Service\Links\LinkSetModel; define('OBJECT_PROPERTIES_TAB', 'ObjectProperties'); @@ -663,10 +667,11 @@ HTML continue; } - // Display mode - if (!$oAttDef->IsLinkset()) { + // Process only link set attributes with tab display style + $bIsLinkSetWithDisplayStyleTab = is_a($oAttDef, AttributeLinkedSet::class) && $oAttDef->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB; + if (!$oAttDef->IsLinkset() || !$bIsLinkSetWithDisplayStyleTab) { continue; - } // Process only linkset attributes... + } $sLinkedClass = $oAttDef->GetLinkedClass(); @@ -897,7 +902,8 @@ HTML // the caller may override some flags if needed $iFlags = $iFlags | $aExtraFlags[$sAttCode]; } - if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard)) { + $bIsLinkSetWithDisplayStyleTab = is_a($oAttDef, AttributeLinkedSet::class) && $oAttDef->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB; + if ((($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard) && !$bIsLinkSetWithDisplayStyleTab) { $sInputId = $this->m_iFormId.'_'.$sAttCode; if ($oAttDef->IsWritable()) { $sInputType = ''; @@ -905,8 +911,8 @@ HTML // State attribute is always read-only from the UI $sHTMLValue = $this->GetAsHTML($sAttCode); $val = array( - 'label' => '', - 'value' => $sHTMLValue, + 'label' => '', + 'value' => $sHTMLValue, 'input_id' => $sInputId, 'comments' => $sComments, 'infos' => $sInfos, @@ -935,7 +941,11 @@ HTML } else { $sValue = $this->Get($sAttCode); $sDisplayValue = $this->GetEditValue($sAttCode); + // transfer bulk context to components as it can be needed (linked set) $aArgs = array('this' => $this, 'formPrefix' => $sPrefix); + if (array_key_exists('bulk_context', $aExtraParams)) { + $aArgs['bulk_context'] = $aExtraParams['bulk_context']; + } $sHTMLValue = "".self::GetFormElementForField( $oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs, @@ -2019,7 +2029,15 @@ HTML } $sHTMLValue = ''; - if (!$oAttDef->IsExternalField()) { + + // attributes not compatible with bulk modify + $bAttNotCompatibleWithBulk = array_key_exists('bulk_context', $aArgs) && !$oAttDef->IsBulkModifyCompatible(); + if ($bAttNotCompatibleWithBulk) { + $oTagSetBlock = new Html(''.Dict::S('UI:Bulk:modify:IncompatibleAttribute').''); + $sHTMLValue = ConsoleBlockRenderer::RenderBlockTemplateInPage($oPage, $oTagSetBlock); + } + + if (!$oAttDef->IsExternalField() && !$bAttNotCompatibleWithBulk) { $bMandatory = 'false'; if ((!$oAttDef->IsNullAllowed()) || ($iFlags & OPT_ATT_MANDATORY)) { $bMandatory = 'true'; @@ -2313,17 +2331,29 @@ EOF break; case 'LinkedSet': - $sInputType = self::ENUM_INPUT_TYPE_LINKEDSET; - if ($oAttDef->IsIndirect()) { - $oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix, - $oAttDef->DuplicatesAllowed()); + if ($oAttDef->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_PROPERTY) { + if (array_key_exists('bulk_context', $aArgs)) { + $oTagSetBlock = LinksSetUIBlockFactory::MakeForBulkLinkSet($iId, $oAttDef, $value, $sWizardHelperJsVarName, $aArgs['bulk_context']); + } else { + $oTagSetBlock = LinksSetUIBlockFactory::MakeForLinkSet($iId, $oAttDef, $value, $sWizardHelperJsVarName); + } + $oTagSetBlock->SetName("attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}"); + $aEventsList[] = 'validate'; + $aEventsList[] = 'change'; + $sHTMLValue = ConsoleBlockRenderer::RenderBlockTemplateInPage($oPage, $oTagSetBlock); } else { - $oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iId, $sNameSuffix); + $sInputType = self::ENUM_INPUT_TYPE_LINKEDSET; + if ($oAttDef->IsIndirect()) { + $oWidget = new UILinksWidget($sClass, $sAttCode, $iId, $sNameSuffix, + $oAttDef->DuplicatesAllowed()); + } else { + $oWidget = new UILinksWidgetDirect($sClass, $sAttCode, $iId, $sNameSuffix); + } + $aEventsList[] = 'validate'; + $aEventsList[] = 'change'; + $oObj = $aArgs['this'] ?? null; + $sHTMLValue = $oWidget->Display($oPage, $value, array(), $sFormPrefix, $oObj); } - $aEventsList[] = 'validate'; - $aEventsList[] = 'change'; - $oObj = isset($aArgs['this']) ? $aArgs['this'] : null; - $sHTMLValue = $oWidget->Display($oPage, $value, array(), $sFormPrefix, $oObj); break; case 'Document': @@ -3550,17 +3580,14 @@ EOF protected function GetFieldAsHtml($sClass, $sAttCode, $sStateAttCode) { $retVal = null; - if ($this->IsNew()) - { + if ($this->IsNew()) { $iFlags = $this->GetInitialStateAttributeFlags($sAttCode); - } - else - { + } else { $iFlags = $this->GetAttributeFlags($sAttCode); } $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); - if ((!$oAttDef->IsLinkSet()) && (($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard)) - { + $bIsLinkSetWithDisplayStyleTab = is_a($oAttDef, AttributeLinkedSet::class) && $oAttDef->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB; + if ((($iFlags & OPT_ATT_HIDDEN) == 0) && !($oAttDef instanceof AttributeDashboard) && !$bIsLinkSetWithDisplayStyleTab) { // First prepare the label // - Attribute description $sDescription = $oAttDef->GetDescription(); @@ -3989,14 +4016,17 @@ HTML; foreach ($value['to_be_created'] as $aData) { $sSubClass = $aData['class']; - if (($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass))) - { + if (($sLinkedClass == $sSubClass) || (is_subclass_of($sSubClass, $sLinkedClass))) { $aObjData = $aData['data']; - $oLink = MetaModel::NewObject($sSubClass); - $oLink->UpdateObjectFromArray($aObjData); - $oLinkSet->AddItem($oLink); + // Avoid duplicates on bulk modify + if (!in_array($aObjData[$oAttDef->GetExtKeyToRemote()], $oLinkSet->GetColumnAsArray($oAttDef->GetExtKeyToRemote(), false)) + || $oAttDef->DuplicatesAllowed()) { + $oLink = MetaModel::NewObject($sSubClass); + $oLink->UpdateObjectFromArray($aObjData); + $oLinkSet->AddItem($oLink); + } } - } + } } if (array_key_exists('to_be_added', $value) && (count($value['to_be_added']) > 0)) { @@ -4202,28 +4232,29 @@ HTML; case 'LinkedSet': /** @var AttributeLinkedSet $oAttDef */ + if ($oAttDef->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_PROPERTY) { + $sLinkedClass = LinkSetModel::GetLinkedClass($oAttDef); + $sTargetField = LinkSetModel::GetTargetField($oAttDef); + $aOperations = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_operations", '{}', 'raw_data'), true); + $value = LinkSetDataTransformer::Encode($aOperations, $sLinkedClass, $sTargetField); + break; + } $aRawToBeCreated = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbc", '{}', 'raw_data'), true); $aToBeCreated = array(); - foreach($aRawToBeCreated as $aData) - { + foreach ($aRawToBeCreated as $aData) { $sSubFormPrefix = $aData['formPrefix']; $sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass(); $aObjData = array(); - foreach($aData as $sKey => $value) - { - if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) - { + foreach ($aData as $sKey => $value) { + if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) { $oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]); // Recursing over n:n link datetime attributes // Note: We might need to do it with other attribute types, like Document or redundancy setting. - if ($oLinkAttDef instanceof AttributeDateTime) - { + if ($oLinkAttDef instanceof AttributeDateTime) { $aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix, $aMatches[1], $sObjClass, $aData); - } - else - { + } else { $aObjData[$aMatches[1]] = $value; } } @@ -4234,25 +4265,19 @@ HTML; $aRawToBeModified = json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbm", '{}', 'raw_data'), true); $aToBeModified = array(); - foreach($aRawToBeModified as $iObjKey => $aData) - { + foreach($aRawToBeModified as $iObjKey => $aData) { $sSubFormPrefix = $aData['formPrefix']; $sObjClass = isset($aData['class']) ? $aData['class'] : $oAttDef->GetLinkedClass(); $aObjData = array(); - foreach($aData as $sKey => $value) - { - if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) - { + foreach($aData as $sKey => $value) { + if (preg_match("/^attr_$sSubFormPrefix(.*)$/", $sKey, $aMatches)) { $oLinkAttDef = MetaModel::GetAttributeDef($sObjClass, $aMatches[1]); // Recursing over n:n link datetime attributes // Note: We might need to do it with other attribute types, like Document or redundancy setting. - if ($oLinkAttDef instanceof AttributeDateTime) - { + if ($oLinkAttDef instanceof AttributeDateTime) { $aObjData[$aMatches[1]] = $this->PrepareValueFromPostedForm($sSubFormPrefix, $aMatches[1], $sObjClass, $aData); - } - else - { + } else { $aObjData[$aMatches[1]] = $value; } } @@ -4261,13 +4286,13 @@ HTML; } $value = array( - 'to_be_created' => $aToBeCreated, + 'to_be_created' => $aToBeCreated, 'to_be_modified' => $aToBeModified, - 'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', + 'to_be_deleted' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbd", '[]', 'raw_data'), true), - 'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]', + 'to_be_added' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tba", '[]', 'raw_data'), true), - 'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]', + 'to_be_removed' => json_decode(utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}_tbr", '[]', 'raw_data'), true), ); break; @@ -4275,7 +4300,9 @@ HTML; case 'Set': case 'TagSet': $sTagSetJson = utils::ReadPostedParam("attr_{$sFormPrefix}{$sAttCode}", null, 'raw_data'); + if ($sTagSetJson !== null) { // bulk modify, direct linked set not handled $value = json_decode($sTagSetJson, true); + } break; default: @@ -4896,13 +4923,12 @@ HTML $sOQL = "SELECT $sClass WHERE id IN (".$sSelectedObj.")"; $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL)); - // Compute the distribution of the values for each field to determine which of the "scalar" fields are homogeneous + // Compute the distribution of the values for each field to determine which of the "scalar or linked set" fields are homogeneous $aList = MetaModel::ListAttributeDefs($sClass); $aValues = array(); foreach($aList as $sAttCode => $oAttDef) { - if ($oAttDef->IsScalar()) - { + if ($oAttDef->IsBulkModifyCompatible()) { $aValues[$sAttCode] = array(); } } @@ -4910,26 +4936,27 @@ HTML { foreach($aList as $sAttCode => $oAttDef) { - if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) - { + if ($oAttDef->IsBulkModifyCompatible() && $oAttDef->IsWritable()) { $currValue = $oObj->Get($sAttCode); - if ($oAttDef instanceof AttributeCaseLog) - { + $editValue = ''; + if ($oAttDef instanceof AttributeCaseLog) { $currValue = ''; // Put a single scalar value to force caselog to mock a new entry. For more info see N°1059. - } - elseif ($currValue instanceof ormSet) - { + } elseif ($currValue instanceof ormSet) { $currValue = $oAttDef->GetEditValue($currValue, $oObj); + } else if ($currValue instanceof ormLinkSet) { + $sHtmlValue = $oAttDef->GetAsHTML($currValue); + $editValue = $oAttDef->GetEditValue($currValue, $oObj); + $currValue = $sHtmlValue; } - if (is_object($currValue)) - { + if (is_object($currValue)) { continue; } // Skip non scalar values... if (!array_key_exists($currValue, $aValues[$sAttCode])) { $aValues[$sAttCode][$currValue] = array( - 'count' => 1, - 'display' => $oObj->GetAsHTML($sAttCode), + 'count' => 1, + 'display' => $oObj->GetAsHTML($sAttCode), + 'edit_value' => $editValue, ); } else @@ -4971,7 +4998,7 @@ HTML $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aDependents)."']"; $oP->add_ready_script("$('#enable_{$sFormPrefix}{$sAttCode}').on('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); } - if ($oAttDef->IsScalar() && $oAttDef->IsWritable()) { + if ($oAttDef->IsBulkModifyCompatible() && $oAttDef->IsWritable()) { if ($oAttDef->GetEditClass() == 'One Way Password') { $sTip = Dict::S('UI:Component:Field:BulkModify:UnknownValues:Tooltip'); @@ -4987,7 +5014,13 @@ HTML reset($aValues[$sAttCode]); $aKeys = array_keys($aValues[$sAttCode]); $currValue = $aKeys[0]; // The only value is the first key - $oDummyObj->Set($sAttCode, $currValue); + if ($oAttDef->GetEditClass() == 'LinkedSet') { + $oOrmLinkSet = $oDummyObj->Get($sAttCode); + LinkSetDataTransformer::StringToOrmLinkSet($aValues[$sAttCode][$currValue]['edit_value'], $oOrmLinkSet); + + } else { + $oDummyObj->Set($sAttCode, $currValue); + } $aComments[$sAttCode] = ''; $sValueCheckbox = ''; if ($sAttCode != MetaModel::GetStateAttributeCode($sClass) || !MetaModel::HasLifecycle($sClass)) { @@ -5035,6 +5068,12 @@ HTML $oTagSet->GenerateDiffFromArray($aTagCodes); } $oDummyObj->Set($sAttCode, $oTagSet); + } else if ($oAttDef->GetEditClass() == 'LinkedSet') { + $oOrmLinkSet = $oDummyObj->Get($sAttCode); + foreach ($aMultiValues as $key => $sValue) { + LinkSetDataTransformer::StringToOrmLinkSet($sValue['edit_value'], $oOrmLinkSet); + } + } else { $oDummyObj->Set($sAttCode, null); } @@ -5070,15 +5109,18 @@ HTML $aParams = array ( - 'fieldsComments' => $aComments, - 'noRelations' => true, + 'fieldsComments' => $aComments, + 'noRelations' => true, 'custom_operation' => $sCustomOperation, - 'custom_button' => Dict::S('UI:Button:PreviewModifications'), - 'selectObj' => $sSelectedObj, - 'nbBulkObj' => $iAllowedCount, - 'preview_mode' => true, - 'disabled_fields' => $sDisableFields, - 'disable_plugins' => true, + 'custom_button' => Dict::S('UI:Button:PreviewModifications'), + 'selectObj' => $sSelectedObj, + 'nbBulkObj' => $iAllowedCount, + 'preview_mode' => true, + 'disabled_fields' => $sDisableFields, + 'disable_plugins' => true, + 'bulk_context' => [ + 'oql' => $sOQL, + ], ); $aParams = $aParams + $aContextData; // merge keeping associations diff --git a/application/wizardhelper.class.inc.php b/application/wizardhelper.class.inc.php index fd6f0a7df..c02f21329 100644 --- a/application/wizardhelper.class.inc.php +++ b/application/wizardhelper.class.inc.php @@ -60,16 +60,20 @@ class WizardHelper // special handling for lists // assumes this is handled as an array of objects // thus encoded in json like: [ { name:'link1', 'id': 123}, { name:'link2', 'id': 124}...] - $aData = json_decode($value, true); // true means decode as a hash array (not an object) + if (!is_array($value)) { + $aData = json_decode($value, true); // true means decode as a hash array (not an object) + } else { + $aData = $value; + } + // Check what are the meaningful attributes $aFields = $this->GetLinkedWizardStructure($oAttDef); $sLinkedClass = $oAttDef->GetLinkedClass(); $aLinkedObjectsArray = array(); - if (!is_array($aData)) - { - echo ("aData: '$aData' (value: '$value')\n"); + if (!is_array($aData)) { + echo("aData: '$aData' (value: '$value')\n"); } - foreach($aData as $aLinkedObject) + foreach ($aData as $aLinkedObject) { $oLinkedObj = MetaModel::NewObject($sLinkedClass); foreach($aFields as $sLinkedAttCode) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 7f04f4c9b..ce11729dd 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -5,12 +5,15 @@ */ use Combodo\iTop\Application\UI\Base\Component\FieldBadge\FieldBadgeUIBlockFactory; +use Combodo\iTop\Application\UI\Links\Indirect\BlockLinksSetDisplayAsProperty; use Combodo\iTop\Form\Field\LabelField; use Combodo\iTop\Form\Field\TextAreaField; use Combodo\iTop\Form\Form; use Combodo\iTop\Form\Validator\NotEmptyExtKeyValidator; use Combodo\iTop\Form\Validator\Validator; use Combodo\iTop\Renderer\BlockRenderer; +use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer; +use Combodo\iTop\Service\Links\LinkSetModel; require_once('MyHelpers.class.inc.php'); require_once('ormdocument.class.inc.php'); @@ -92,6 +95,9 @@ define('LINKSET_EDITMODE_ADDREMOVE', 4); // The "linked" objects can be added/re define('LINKSET_RELATIONTYPE_PROPERTY', 'property'); define('LINKSET_RELATIONTYPE_LINK', 'link'); +define('LINKSET_DISPLAY_STYLE_PROPERTY', 'property'); +define('LINKSET_DISPLAY_STYLE_TAB', 'tab'); + /** * Attributes implementing this interface won't be accepted as `group by` field * @@ -140,6 +146,15 @@ abstract class AttributeDefinition abstract public function GetEditClass(); + /** + * @return array Css classes + * @since 3.1.0 N°3190 + */ + public function GetCssClasses(): array + { + return $this->aCSSClasses; + } + /** * Return the search widget type corresponding to this attribute * @@ -361,6 +376,18 @@ abstract class AttributeDefinition return false; } + /** + * Returns true if the attribute can be used in bulk modify. + * + * @return bool + * @since 3.1.0 N°3190 + * + */ + public static function IsBulkModifyCompatible(): bool + { + return static::IsScalar(); + } + /** * Returns true if the attribute value is a set of related objects (1-N or N-N) * @@ -1417,6 +1444,7 @@ class AttributeLinkedSet extends AttributeDefinition public function __construct($sCode, $aParams) { parent::__construct($sCode, $aParams); + $this->aCSSClasses[] = 'attribute-set'; } public static function ListExpectedParams() @@ -1430,6 +1458,12 @@ class AttributeLinkedSet extends AttributeDefinition return "LinkedSet"; } + /** @inheritDoc */ + public static function IsBulkModifyCompatible(): bool + { + return false; + } + public function IsWritable() { return true; @@ -1447,7 +1481,26 @@ class AttributeLinkedSet extends AttributeDefinition public function GetValuesDef() { - return $this->Get("allowed_values"); + $oValSetDef = $this->Get("allowed_values"); + if (!$oValSetDef) { + // Let's propose every existing value + $oValSetDef = new ValueSetObjects('SELECT '.LinkSetModel::GetTargetClass($this)); + } + + return $oValSetDef; + } + + public function GetEditValue($value, $oHostObj = null) + { + /** @var ormLinkSet $value * */ + if ($value->Count() === 0) { + return ''; + } + + /** Return linked objects key as string **/ + $aValues = $value->GetValues(); + + return implode(' ', $aValues); } public function GetPrerequisiteAttributes($sClass = null) @@ -1532,6 +1585,15 @@ class AttributeLinkedSet extends AttributeDefinition return $this->GetOptional('relation_type', LINKSET_RELATIONTYPE_LINK); } + /** + * @return string see LINKSET_DISPLAY_STYLE_* constants + * @since 3.1.0 N°3190 + */ + public function GetDisplayStyle() + { + return $this->GetOptional('display_style', LINKSET_DISPLAY_STYLE_TAB); + } + /** * @return boolean * @since 3.1.0 N°5563 @@ -1566,49 +1628,30 @@ class AttributeLinkedSet extends AttributeDefinition return ''; } - /** - * @param string $sValue - * @param \DBObject $oHostObject - * @param bool $bLocalize - * - * @return string|null - * - * @throws \CoreException - */ - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + /** @inheritDoc * */ + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true): string { - if (is_object($sValue) && ($sValue instanceof ormLinkSet)) - { - $sValue->Rewind(); - $aItems = array(); - while ($oObj = $sValue->Fetch()) - { - // Show only relevant information (hide the external key to the current object) - $aAttributes = array(); - foreach(MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) - { - if ($sAttCode == $this->GetExtKeyToMe()) - { - continue; - } - if ($oAttDef->IsExternalField()) - { - continue; - } - $sAttValue = $oObj->GetAsHTML($sAttCode); - if (strlen($sAttValue) > 0) - { - $aAttributes[] = $sAttValue; - } - } - $sAttributes = implode(', ', $aAttributes); - $aItems[] = $sAttributes; + try { + + /** @var ormLinkSet $sValue */ + if ($sValue->Count() === 0) { + return ''; } - return implode('
', $aItems); - } + $oLinkSetBlock = new BlockLinksSetDisplayAsProperty($this->GetCode(), $this, $sValue); - return null; + return ConsoleBlockRenderer::RenderBlockTemplates($oLinkSetBlock); + } + catch (Exception $e) { + $sMessage = "Error while displaying attribute {$this->GetCode()}"; + IssueLog::Error($sMessage, IssueLog::CHANNEL_DEFAULT, [ + 'host_object_class' => $this->GetHostClass(), + 'host_object_key' => $oHostObject->GetKey(), + 'attribute' => $this->GetCode(), + ]); + + return $sMessage; + } } /** @@ -2361,6 +2404,13 @@ class AttributeLinkedSetIndirect extends AttributeLinkedSet return $oRet; } + + /** @inheritDoc */ + public static function IsBulkModifyCompatible(): bool + { + return true; + } + } /** diff --git a/core/ormlinkset.class.inc.php b/core/ormlinkset.class.inc.php index 0f83199f6..65ea0dca2 100644 --- a/core/ormlinkset.class.inc.php +++ b/core/ormlinkset.class.inc.php @@ -847,11 +847,30 @@ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator } $oLinkSet = new DBObjectSet($oLinkSearch); $oLinkSet->SetShowObsoleteData($bShowObsolete); - if ($this->HasDelta()) - { + if ($this->HasDelta()) { $oLinkSet->AddObjectArray($this->aAdded); } return $oLinkSet; } + + /** + * GetValues. + * + * @return array of tag codes + */ + public function GetValues() + { + $aValues = array(); + foreach ($this->aPreserved as $sTagCode => $oTag) { + $aValues[] = $sTagCode; + } + foreach ($this->aAdded as $sTagCode => $oTag) { + $aValues[] = $sTagCode; + } + + sort($aValues); + + return $aValues; + } } diff --git a/css/backoffice/application/_all.scss b/css/backoffice/application/_all.scss index c4637a11f..5c0b5699e 100644 --- a/css/backoffice/application/_all.scss +++ b/css/backoffice/application/_all.scss @@ -3,5 +3,7 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +@import "bulk/all"; @import "display-block/all"; +@import "linked-set/all"; @import "tabular-fields/all"; \ No newline at end of file diff --git a/css/backoffice/application/bulk/_all.scss b/css/backoffice/application/bulk/_all.scss new file mode 100644 index 000000000..2e80aecbb --- /dev/null +++ b/css/backoffice/application/bulk/_all.scss @@ -0,0 +1,6 @@ +/* + * @copyright Copyright (C) 2010-2022 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +@import "bulk-modify"; diff --git a/css/backoffice/application/bulk/_bulk-modify.scss b/css/backoffice/application/bulk/_bulk-modify.scss new file mode 100644 index 000000000..b84141b43 --- /dev/null +++ b/css/backoffice/application/bulk/_bulk-modify.scss @@ -0,0 +1,14 @@ +/* + * @copyright Copyright (C) 2010-2022 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +.ibo-bulk--bulk-modify--incompatible-attribute { + + &:before{ + margin-right: $ibo-vendors-selectize--item--icon--margin-right; + @extend %fa-solid-base; + content: '\f05a'; + color: $ibo-color-information-500; + } +} \ No newline at end of file diff --git a/css/backoffice/application/linked-set/_all.scss b/css/backoffice/application/linked-set/_all.scss new file mode 100644 index 000000000..37625228c --- /dev/null +++ b/css/backoffice/application/linked-set/_all.scss @@ -0,0 +1,6 @@ +/* + * @copyright Copyright (C) 2010-2022 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +@import "linked-set-selector"; \ No newline at end of file diff --git a/css/backoffice/application/linked-set/_linked-set-selector.scss b/css/backoffice/application/linked-set/_linked-set-selector.scss new file mode 100644 index 000000000..ecbd687cf --- /dev/null +++ b/css/backoffice/application/linked-set/_linked-set-selector.scss @@ -0,0 +1,14 @@ +/* + * @copyright Copyright (C) 2010-2022 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +.ibo-linked-set--bulk-tooltip-info { + font-size: $ibo-font-size-100; + &:before{ + margin-right: $ibo-vendors-selectize--item--icon--margin-right; + @extend %fa-solid-base; + content: '\f05a'; + color: $ibo-color-information-500; + } +} \ No newline at end of file diff --git a/css/backoffice/vendors/_selectize.scss b/css/backoffice/vendors/_selectize.scss index ea917cd4d..a4d97183e 100644 --- a/css/backoffice/vendors/_selectize.scss +++ b/css/backoffice/vendors/_selectize.scss @@ -4,10 +4,95 @@ */ $ibo-vendors-selectize-input--color: $ibo-color-grey-900 !default; +$ibo-vendors-selectize-control--plugin-add-button--add-option--right: $ibo-spacing-0 !default; +$ibo-vendors-selectize-control--plugin-add-button--add-option--height: 100% !default; +$ibo-vendors-selectize-control--plugin-add-button--add-option--width: $ibo-size-350 !default; +$ibo-vendors-selectize-control--plugin-add-button--add-option--color: $ibo-color-grey-900 !default; + +$ibo-vendors-selectize--item--icon--margin-right: $ibo-spacing-200 !default; + +$ibo-vendors-selectize--item--add--background-color: $ibo-color-green-100 !default; +$ibo-vendors-selectize--item--add--icon--color: $ibo-color-green-900 !default; + +$ibo-vendors-selectize--item--remove--background-color: $ibo-color-red-100 !default; +$ibo-vendors-selectize--item--remove--icon--color: $ibo-color-red-800 !default; + +$ibo-vendors-selectize--item--ignore-partial--background-color: $ibo-color-grey-200 !default; + +$ibo-vendors-selectize--input-error--border: 1px solid $ibo-color-red-600 !default; .selectize-dropdown-content { max-height: unset; /* Overloaded as it will be handled by the _input-select.scss partial */ } .selectize-input input{ color: $ibo-vendors-selectize-input--color; +} + + +.selectize-control.plugin-combodo_add_button{ + display: flex; + + .selectize-add-option { + position: absolute; + right: $ibo-vendors-selectize-control--plugin-add-button--add-option--right; + display: inline-flex; + justify-content: center; + align-items: center; + height: $ibo-vendors-selectize-control--plugin-add-button--add-option--height; + width: $ibo-vendors-selectize-control--plugin-add-button--add-option--width; + z-index: 1; + color: $ibo-vendors-selectize-control--plugin-add-button--add-option--color; + } +} + + +// Simple options renderer + +.simple-option-renderer--container { + display: flex; + align-items: center; +} + +.simple-option-renderer--container--icon { + width: 25px; + text-align: center; +} + +.simple-option-renderer--container--label { + margin-left: 3px; + flex-grow: 1; +} + + + +.selectize-input{ + .attribute-set-item{ + >* { + display: inline; + } + &.item-add::before,&.item-remove::before{ + @extend %fa-solid-base; + margin-right: $ibo-vendors-selectize--item--icon--margin-right; + } + &.item-add{ + background-color: $ibo-vendors-selectize--item--add--background-color !important; + &::before{ + color: $ibo-vendors-selectize--item--add--icon--color; + content: '\f067'; + } + } + &.item-remove{ + background-color: $ibo-vendors-selectize--item--remove--background-color !important; + &::before{ + color: $ibo-vendors-selectize--item--remove--icon--color; + content: '\f1f8'; + } + } + &.item-ignore-partial{ + background-color: $ibo-vendors-selectize--item--ignore-partial--background-color !important; + } + } + &.selectize-input-error{ + border: $ibo-vendors-selectize--input-error--border; + } } \ No newline at end of file diff --git a/dictionaries/ui/application/bulk/en.dictionary.itop.bulk.php b/dictionaries/ui/application/bulk/en.dictionary.itop.bulk.php new file mode 100644 index 000000000..367137057 --- /dev/null +++ b/dictionaries/ui/application/bulk/en.dictionary.itop.bulk.php @@ -0,0 +1,25 @@ + 'This attribute can\'t be edited in bulk context', + +)); \ No newline at end of file diff --git a/dictionaries/ui/application/bulk/fr.dictionary.itop.bulk.php b/dictionaries/ui/application/bulk/fr.dictionary.itop.bulk.php new file mode 100644 index 000000000..cb0b7faee --- /dev/null +++ b/dictionaries/ui/application/bulk/fr.dictionary.itop.bulk.php @@ -0,0 +1,25 @@ + 'Cet attribut ne peut être édité dans une modification en masse', + +)); \ No newline at end of file diff --git a/dictionaries/ui/application/links/en.dictionary.itop.links.php b/dictionaries/ui/application/links/en.dictionary.itop.links.php index 17694e155..89b199f1d 100644 --- a/dictionaries/ui/application/links/en.dictionary.itop.links.php +++ b/dictionaries/ui/application/links/en.dictionary.itop.links.php @@ -17,17 +17,31 @@ * You should have received a copy of the GNU Affero General Public License */ -// Display DataTable Dict::Add('EN US', 'English', 'English', array( - 'UI:Links:ActionRow:Detach' => 'Detach', - 'UI:Links:ActionRow:Detach+' => 'Detach this object', - 'UI:Links:ActionRow:Detach:Confirmation' => 'Do you really want to detach {item} from current object ?', - 'UI:Links:ActionRow:Delete' => 'Delete', - 'UI:Links:ActionRow:Delete+' => 'Delete this object', - 'UI:Links:ActionRow:Delete:Confirmation' => 'Do you really want to delete {item} from current object ?', - 'UI:Links:ActionRow:Modify' => 'Modify', - 'UI:Links:ActionRow:Modify+' => 'Modify this object', - 'UI:Links:ActionRow:Modify:Modal:Title' => 'Modify a link', - 'UI:Links:New:Modal:Title' => 'Creation of a link', - 'UI:Links:New:Button:Tooltip' => 'Add a new link', + + // Action row + 'UI:Links:ActionRow:Detach' => 'Detach', + 'UI:Links:ActionRow:Detach+' => 'Detach this object', + 'UI:Links:ActionRow:Detach:Confirmation' => 'Do you really want to detach {item} from current object ?', + 'UI:Links:ActionRow:Delete' => 'Delete', + 'UI:Links:ActionRow:Delete+' => 'Delete this object', + 'UI:Links:ActionRow:Delete:Confirmation' => 'Do you really want to delete {item} from current object ?', + 'UI:Links:ActionRow:Modify' => 'Modify', + 'UI:Links:ActionRow:Modify+' => 'Modify this object', + 'UI:Links:ActionRow:Modify:Modal:Title' => 'Modify a link', + + // New + 'UI:Links:New:Modal:Title' => 'Creation of a link', + 'UI:Links:New:Button:Tooltip' => 'Add a new link', + + // Bulk + 'UI:Links:Bulk:LinkWillBeCreatedForAllObjects' => 'Link all objects', + 'UI:Links:Bulk:LinkWillBeDeletedFromAllObjects' => 'Unlink all objects', + 'UI:Links:Bulk:LinkWillBeCreatedFor1Object' => 'Link one object', + 'UI:Links:Bulk:LinkWillBeDeletedFrom1Object' => 'Unlink one object', + 'UI:Links:Bulk:LinkWillBeCreatedForXObjects' => 'Link {count} objects', + 'UI:Links:Bulk:LinkWillBeDeletedFromXObjects' => 'Unlink {count} objects', + 'UI:Links:Bulk:LinkExistForAllObjects' => 'All objets are already linked', + 'UI:Links:Bulk:LinkExistForOneObject' => 'One object is linked', + 'UI:Links:Bulk:LinkExistForXObjects' => '{count} objects are linked', )); \ No newline at end of file diff --git a/dictionaries/ui/application/links/fr.dictionary.itop.links.php b/dictionaries/ui/application/links/fr.dictionary.itop.links.php index e6e8b8553..2a260ccdb 100644 --- a/dictionaries/ui/application/links/fr.dictionary.itop.links.php +++ b/dictionaries/ui/application/links/fr.dictionary.itop.links.php @@ -17,12 +17,24 @@ * You should have received a copy of the GNU Affero General Public License */ -// Display DataTable Dict::Add('FR FR', 'French', 'Français', array( - 'UI:Links:ActionRow:Detach' => 'Détacher', - 'UI:Links:ActionRow:Detach+' => 'Détacher cet objet', - 'UI:Links:ActionRow:Detach:Confirmation' => 'Voulez-vous détacher {item} de l\'objet courant ?', - 'UI:Links:ActionRow:Delete' => 'Supprimer', - 'UI:Links:ActionRow:Delete+' => 'Supprimer cet objet', - 'UI:Links:ActionRow:Delete:Confirmation' => 'Voulez-vous supprimer {item} de l\'objet courant ?', + + // Action row + 'UI:Links:ActionRow:Detach' => 'Détacher', + 'UI:Links:ActionRow:Detach+' => 'Détacher cet objet', + 'UI:Links:ActionRow:Detach:Confirmation' => 'Voulez-vous détacher {item} de l\'objet courant ?', + 'UI:Links:ActionRow:Delete' => 'Supprimer', + 'UI:Links:ActionRow:Delete+' => 'Supprimer cet objet', + 'UI:Links:ActionRow:Delete:Confirmation' => 'Voulez-vous supprimer {item} de l\'objet courant ?', + + // Bulk + 'UI:Links:Bulk:LinkWillBeCreatedForAllObjects' => 'Lier à tous les objets', + 'UI:Links:Bulk:LinkWillBeDeletedFromAllObjects' => 'Détacher de tous les objets', + 'UI:Links:Bulk:LinkWillBeCreatedFor1Object' => 'Lier à un objet', + 'UI:Links:Bulk:LinkWillBeDeletedFrom1Object' => 'Détacher de un objet', + 'UI:Links:Bulk:LinkWillBeCreatedForXObjects' => 'Lier à {count} objets', + 'UI:Links:Bulk:LinkWillBeDeletedFromXObjects' => 'Détacher de {count} objets', + 'UI:Links:Bulk:LinkExistForAllObjects' => 'Tous les objets sont déjà liés', + 'UI:Links:Bulk:LinkExistForOneObject' => 'Un objet est lié', + 'UI:Links:Bulk:LinkExistForXObjects' => '{count} objets sont liés', )); \ No newline at end of file diff --git a/js/selectize/plugin_combodo_add_button.js b/js/selectize/plugin_combodo_add_button.js new file mode 100644 index 000000000..f7ae2370a --- /dev/null +++ b/js/selectize/plugin_combodo_add_button.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013-2022 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ + +Selectize.define("combodo_add_button", function (aOptions) { + + // Selectize instance + let oSelf = this; + + // Plugin options + aOptions = $.extend({ + title: "Add Option", + className: "selectize-add-option", + label: "+", + html: function () { + return ( + '' + ); + }, + }, + aOptions + ); + + // Override setup function + oSelf.setup = (function () { + let oOriginal = oSelf.setup; + return function () { + oOriginal.apply(oSelf, arguments); + oSelf.$buttonAdd = $(aOptions.html()); + oSelf.$wrapper.append(oSelf.$buttonAdd); + if(oSelf.settings.hasOwnProperty('onAdd')) { + oSelf.on('add', oSelf.settings['onAdd']); + oSelf.$buttonAdd.on('click', function(){ + oSelf.trigger( "add"); + }); + } + else{ + oSelf.$buttonAdd.css({ + opacity: .5, + cursor: 'default' + }); + } + }; + })(); + +}); \ No newline at end of file diff --git a/js/selectize/plugin_combodo_auto_position.js b/js/selectize/plugin_combodo_auto_position.js new file mode 100644 index 000000000..b6a76bb98 --- /dev/null +++ b/js/selectize/plugin_combodo_auto_position.js @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013-2022 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ +Selectize.define("combodo_auto_position", function (aOptions) { + + // Selectize instance + let oSelf = this; + + // Plugin options + aOptions = $.extend({ + maxDropDownHeight: 200, + }, + aOptions + ); + + // override settings + oSelf.settings.dropdownParent = 'body'; + + // Override position dropdown function + oSelf.positionDropdown = (function () { + return function () { + let iRefHeight = oSelf.$dropdown.outerHeight() < aOptions.maxDropDownHeight ? + oSelf.$dropdown.outerHeight() : aOptions.maxDropDownHeight; + + if(oSelf.$control.offset().top + oSelf.$control.outerHeight() + iRefHeight > window.innerHeight){ + + oSelf.$dropdown.css({ + top: oSelf.$control.offset().top - iRefHeight, + left: oSelf.$control.offset().left, + width: oSelf.$wrapper.outerWidth(), + 'max-height': `${aOptions.maxDropDownHeight}px`, + 'overflow-y': 'auto', + 'border-top': '1px solid #d0d0d0', + }); + } + else{ + oSelf.$dropdown.css({ + top: oSelf.$control.offset().top + oSelf.$control.outerHeight(), + left: oSelf.$control.offset().left, + width: oSelf.$wrapper.outerWidth(), + 'max-height': `${aOptions.maxDropDownHeight}px`, + 'overflow-y': 'auto' + }); + } + }; + }()); + +}); \ No newline at end of file diff --git a/js/selectize/plugin_combodo_multi_values_synthesis.js b/js/selectize/plugin_combodo_multi_values_synthesis.js new file mode 100644 index 000000000..6ffe9d612 --- /dev/null +++ b/js/selectize/plugin_combodo_multi_values_synthesis.js @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2013-2022 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ +Selectize.define("combodo_multi_values_synthesis", function (aOptions) { + + // Selectize instance + let oSelf = this; + oSelf.require("combodo_update_operations"); + + // Plugin options + aOptions = $.extend({ + tooltip_links_will_be_created_for_all_objects: 'Links will be created for all objects', + tooltip_links_will_be_deleted_from_all_objects: 'Links will be deleted from all objects', + tooltip_links_will_be_created_for_one_object: 'Links will be created for one object', + tooltip_links_will_be_deleted_from_one_object: 'Links will be deleted from one object', + tooltip_links_will_be_created_for_x_objects: 'Links will be created for {count} objects', + tooltip_links_will_be_deleted_from_x_objects: 'Links will be deleted from {count} objects', + tooltip_links_exist_for_all_objects: 'Links exist for all objects', + tooltip_links_exist_for_one_object: 'Links exist for one object', + tooltip_links_exist_for_x_objects: 'Links exist for some objects' + }, + aOptions + ); + + // Items operations + const OPERATIONS = { + add: 'add', + remove: 'remove', + ignore: 'ignore', + }; + + // Items states css classes + const ITEMS_CLASSES = { + add: 'item-add', + remove: 'item-remove', + ignore_all: 'item-ignore-all', + ignore_partial: 'item-ignore-partial' + }; + + // Local operations + let aOperations = {}; + + // Override addItem function + oSelf.addItem = (function () { + let oOriginal = oSelf.addItem; + return function () { + + oOriginal.apply(this, arguments); + + // Retrieve item and item element + const sItemValue = arguments[0]; + const $Item = oSelf.getItem(sItemValue); + + // Restore operation if exist and return + if(typeof(aOperations[sItemValue]) !== 'undefined'){ + if(aOperations[sItemValue] === OPERATIONS.add){ + oSelf.Add($Item, sItemValue); + } + else if(aOperations[sItemValue] === OPERATIONS.remove){ + oSelf.Remove($Item, sItemValue); + } + else if(aOperations[sItemValue] === OPERATIONS.ignore){ + oSelf.Ignore($Item, sItemValue); + } + // Element exist in default selection, + // click allow user to switch between add or ignore states + if(oSelf.settings.initial.includes(sItemValue)) { + oSelf.listenClick($Item, sItemValue); + } + return; + } + + // If no operation to restore + if(!oSelf.settings.initial.includes(sItemValue)) { + + // Element doesn't exist in initial value, we mark it as added + oSelf.Add($Item, sItemValue); + } + else{ + + // Element exist, we restore it + oSelf.Ignore($Item, sItemValue); + + // Element exist in default selection, + // click allow user to switch between add or ignore states + oSelf.listenClick($Item, sItemValue); + } + } + })(); + + // Override removeItem function + oSelf.removeItem = (function () { + let oOriginal = oSelf.removeItem; + return function () { + + // Retrieve item and item element + const sItem = arguments[0]; + const $Item = oSelf.getItem(sItem); + + // Element doesn't exist in default selection, + if(!oSelf.settings.initial.includes(sItem)) { + + // Remove operation + delete aOperations[sItem]; + + // Call original remove function (element will be removed of the input) + oOriginal.apply(this, arguments); + } + else{ + + // Store remove operation (element will NOT be removed) + oSelf.Remove($Item, sItem); + } + } + })(); + + // Override updateOperations function + oSelf.updateOperations = (function () { + let oOriginal = oSelf.updateOperations; + return function () { + + // Call original updateOperations function + oOriginal.apply(this, arguments); + + // Iterate throw local operations... + const aCurrentOptions = Object.values(oSelf.options); + for (const [key, value] of Object.entries(aOperations)) { + oSelf.operations[key] = { + operation: value, + data: CombodoGlobalToolbox.ExtractArrayItemsContainingThisKeyAndValue(aCurrentOptions, oSelf.settings.valueField, key) + } + } + } + })(); + + // Declare listenClick function + oSelf.listenClick = (function () { + return function ($item, sItem) { + + // Listen item element click event + $item.on('click', function(){ + + // If element has operation + if(aOperations[sItem] === OPERATIONS.add || aOperations[sItem] === OPERATIONS.remove) { + + // Restore state + oSelf.Ignore($item, sItem); + } + else{ + + // No need to add + if(oSelf.options[sItem]['full']) + return; + + // Add element + oSelf.Add($item, sItem); + } + }); + } + })(); + + // Declare Add function + oSelf.Add = (function () { + return function ($item, sItem) { + aOperations[sItem] = OPERATIONS.add; + oSelf.updateOperationsInput(); + oSelf.ResetElementClass($item); + oSelf.UpdateAllTooltip($item, sItem); + $item.addClass(ITEMS_CLASSES.add); + } + })(); + + // Declare Remove function + oSelf.Remove = (function () { + return function ($item, sItem) { + aOperations[sItem] = OPERATIONS.remove; + oSelf.updateOperationsInput(); + oSelf.ResetElementClass($item); + oSelf.UpdateRemoveTooltip($item, sItem); + $item.addClass(ITEMS_CLASSES.remove); + } + })(); + + // Declare Ignore function + oSelf.Ignore = (function () { + return function ($item, sItem) { + aOperations[sItem] = OPERATIONS.ignore; + oSelf.updateOperationsInput(); + oSelf.ResetElementClass($item); + oSelf.UpdateIgnoreTooltip($item, sItem); + oSelf.options[sItem]['full'] ? + $item.addClass(ITEMS_CLASSES.ignore_all) : + $item.addClass(ITEMS_CLASSES.ignore_partial); + } + })(); + + // Declare ResetElementClass function + oSelf.ResetElementClass = (function () { + return function ($item) { + $item.removeClass(Object.values(ITEMS_CLASSES)); + } + })(); + + // Update add tooltip + oSelf.UpdateAllTooltip = (function () { + return function ($item, sItem) { + const iOccurrence = oSelf.options[sItem]['occurrence']; + let sTooltip = ''; + if(oSelf.options[sItem]['empty']){ + sTooltip = aOptions.tooltip_links_will_be_created_for_all_objects; + } + else if(iOccurrence === '1'){ + sTooltip = aOptions.tooltip_links_will_be_created_for_one_object; + } + else{ + sTooltip = aOptions.tooltip_links_will_be_created_for_x_objects.replaceAll('{count}', iOccurrence); + } + oSelf.CreateTooltip($item, sItem, sTooltip); + } + })(); + + // Update remove tooltip + oSelf.UpdateRemoveTooltip = (function () { + return function ($item, sItem) { + const iOccurrence = oSelf.options[sItem]['occurrence']; + let sTooltip = ''; + if(oSelf.options[sItem]['full']){ + sTooltip = aOptions.tooltip_links_will_be_deleted_from_all_objects; + } + else if(oSelf.options[sItem]['occurrence'] === '1'){ + sTooltip = aOptions.tooltip_links_will_be_deleted_from_one_object; + } + else{ + sTooltip = aOptions.tooltip_links_will_be_deleted_from_x_objects.replaceAll('{count}', iOccurrence); + } + oSelf.CreateTooltip($item, sItem, sTooltip); + } + })(); + + // Update ignore tooltip + oSelf.UpdateIgnoreTooltip = (function () { + return function ($item, sItem) { + const iOccurrence = oSelf.options[sItem]['occurrence']; + let sTooltip = ''; + if(oSelf.options[sItem]['full']){ + sTooltip = aOptions.tooltip_links_exist_for_all_objects; + } + else if(iOccurrence === '1'){ + sTooltip = aOptions.tooltip_links_exist_for_one_object; + } + else{ + sTooltip = aOptions.tooltip_links_exist_for_x_objects.replaceAll('{count}', iOccurrence); + } + oSelf.CreateTooltip($item, sItem, sTooltip); + } + })(); + + // Update ignore tooltip + oSelf.CreateTooltip = (function () { + return function ($item, sItem, sTooltip) { + $item.attr('data-tooltip-content', oSelf.options[sItem][this.settings.tooltipField] + '
' + sTooltip + ''); + $item.attr('data-tooltip-html-enabled', true); + CombodoTooltip.InitTooltipFromMarkup($item, true); + } + })(); +}); \ No newline at end of file diff --git a/js/selectize/plugin_combodo_update_operations.js b/js/selectize/plugin_combodo_update_operations.js new file mode 100644 index 000000000..a129f3969 --- /dev/null +++ b/js/selectize/plugin_combodo_update_operations.js @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013-2022 Combodo SARL + * + * This file is part of iTop. + * + * iTop is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * iTop is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + */ +Selectize.define("combodo_update_operations", function () { + + // Selectize instance + let oSelf = this; + + // Plugin variables + oSelf.bIsInitialized = false; + oSelf.operations = {}; + + // Override setup function + oSelf.setup = (function () { + let oOriginal = oSelf.setup; + return function () { + oOriginal.apply(oSelf, arguments); + oSelf.$operationsInput = $(``) + oSelf.$wrapper.append(oSelf.$operationsInput); + oSelf.bIsInitialized = true; + oSelf.updateOperationsInput(); + }; + })(); + + // Override addItem function + oSelf.addItem = (function () { + let oOriginal = oSelf.addItem; + return function () { + oOriginal.apply(this, arguments); + if(oSelf.bIsInitialized && !arguments[1]){ + this.updateOperationsInput(); + } + } + })(); + + // Override removeItem function + oSelf.removeItem = (function () { + let oOriginal = oSelf.removeItem; + return function () { + oOriginal.apply(this, arguments); + if(oSelf.bIsInitialized){ + this.updateOperationsInput(); + } + } + })(); + + // Declare updateOperationsInput function + oSelf.updateOperationsInput = (function () { + return function () { + + // update operations + oSelf.updateOperations(); + + // setup in progress + if(typeof(oSelf.$operationsInput) === 'undefined'){ + return; + } + + // update operations input + oSelf.$operationsInput.val(JSON.stringify(oSelf.operations)); + }; + })(); + + // Declare updateOperations function + oSelf.updateOperations = (function () { + return function () { + + // Reset operations + oSelf.operations = {}; + + // Reference data + const aCurrentItems = Object.values(oSelf.items); + const aCurrentOptions = Object.values(oSelf.options); + + // Scan items in current value and not in initial value + aCurrentItems.forEach(function(e){ + if(!oSelf.settings.initial.includes(e)){ + oSelf.operations[e] = { + operation: 'add', + data: CombodoGlobalToolbox.ExtractArrayItemsContainingThisKeyAndValue(aCurrentOptions, oSelf.settings.valueField, e) + } + } + }); + + // scan items in initial value and not in current value + oSelf.settings.initial.forEach(function(e){ + if(!aCurrentItems.includes(e)){ + oSelf.operations[e] = { + operation: 'remove', + data: CombodoGlobalToolbox.ExtractArrayItemsContainingThisKeyAndValue(aCurrentOptions, oSelf.settings.valueField, e) + } + } + }); + + }; + })(); + +}); \ No newline at end of file diff --git a/js/utils.js b/js/utils.js index 77e6aa259..4a9b03bdb 100644 --- a/js/utils.js +++ b/js/utils.js @@ -759,6 +759,105 @@ const CombodoGlobalToolbox = { oCurrentDate = new Date(); } while ((oCurrentDate - oDate) < iDuration); + }, + + /** + * Render a template and inject data into it. + * + * This rendering engine is aimed to produce client side template rendering. + * + * markups with attributes: + * data-template-attr-{title|name|for}: set dom element attribute with corresponding datavalue + * data-template-text: set dom element text with corresponding data value + * data-template-condition: set dom element visibility depending on data value + * data-template-css-{background-image}: set dom element css property with corresponding data value + * data-template-add-class: add class to dom element with corresponding data value + * + * @since 3.1.0 + * + * @param sTemplateId + * @param aData + * @param sTemplateClass + * @returns {*|jQuery|HTMLElement|JQuery} + * @constructor + */ + RenderTemplate: function(sTemplateId, aData, sTemplateClass = null) + { + let sHtml = '
' + $(sTemplateId).html() + '
'; + + // Create element + let oElement = $(sHtml); + if(sTemplateClass !== null){ + oElement.addClass(sTemplateClass); + } + + // Attribute replacement + let aAttrElements = ['title', 'name', 'for']; + aAttrElements.forEach(function(e){ + $(`[data-template-attr-${e}]`, oElement).each(function(){ + $(this).attr(e, aData[$(this).attr(`data-template-attr-${e}`)]); + }) + }); + + // CSS replacement + let aCssElements = ['background-image']; + aCssElements.forEach(function(e){ + $(`[data-template-css-${e}]`, oElement).each(function(){ + $(this).css(e, aData[$(this).attr(`data-template-css-${e}`)]); + }) + }); + + // Text replacement + $('[data-template-text]', oElement).each(function(){ + $(this).text(aData[$(this).attr('data-template-text')]); + }) + + // Condition + $('[data-template-condition]', oElement).each(function(){ + $(this).toggle(aData[$(this).attr('data-template-condition')]); + }) + + // Add classes + $('[data-template-add-class]', oElement).each(function(){ + $(this).addClass(aData[$(this).attr('data-template-add-class')]); + }) + + return oElement; + }, + + /** + * ExtractArrayItemsContainingThisKeyAndValue. + * + * This function extract item(s) of an array witch include the key value pair. + * + * @since 3.1.0 + * + * @param aArrayToSearchIn Array to search in + * @param sKey Key to search + * @param sValue Value to search + * @returns {*|*[]|null} + * @constructor + */ + ExtractArrayItemsContainingThisKeyAndValue: function(aArrayToSearchIn, sKey, sValue) + { + let aResult = []; + + // Iterate throw items... + for(let i = 0 ; i < aArrayToSearchIn.length ; i++){ + if(aArrayToSearchIn[i][sKey] === sValue){ + aResult.push(aArrayToSearchIn[i]); + } + } + + // Return result + switch(aResult.length){ + case 0: + return null; + case 1: + return aResult[0]; + default: + return aResult; + } } }; diff --git a/lib/composer/ClassLoader.php b/lib/composer/ClassLoader.php index afef3fa2a..0cd6055d1 100644 --- a/lib/composer/ClassLoader.php +++ b/lib/composer/ClassLoader.php @@ -149,7 +149,7 @@ class ClassLoader /** * @return string[] Array of classname => path - * @psalm-return array + * @psalm-var array */ public function getClassMap() { diff --git a/lib/composer/InstalledVersions.php b/lib/composer/InstalledVersions.php index 7c5502ca4..d50e0c9fc 100644 --- a/lib/composer/InstalledVersions.php +++ b/lib/composer/InstalledVersions.php @@ -24,8 +24,21 @@ use Composer\Semver\VersionParser; */ class InstalledVersions { + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null + */ private static $installed; + + /** + * @var bool|null + */ private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ private static $installedByVendor = array(); /** diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 39c9430fd..1abba84ac 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -254,6 +254,13 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\Select' => $baseDir . '/sources/Application/UI/Base/Component/Input/Select/Select.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\SelectOption' => $baseDir . '/sources/Application/UI/Base/Component/Input/Select/SelectOption.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\SelectOptionUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Input/Select/SelectOptionUIBlockFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AbstractDataProvider' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AjaxDataProvider' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AjaxDataProviderForOQL' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\SimpleDataProvider' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\iDataProvider' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\Set' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/Set.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\SetUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\TextArea' => $baseDir . '/sources/Application/UI/Base/Component/Input/TextArea.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => $baseDir . '/sources/Application/UI/Base/Component/Input/tInputLabel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => $baseDir . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php', @@ -349,7 +356,9 @@ return array( 'Combodo\\iTop\\Application\\UI\\Links\\Direct\\BlockDirectLinksViewTable' => $baseDir . '/sources/Application/UI/Links/Direct/BlockDirectLinksViewTable.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockIndirectLinksEditTable' => $baseDir . '/sources/Application/UI/Links/Indirect/BlockIndirectLinksEditTable.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockIndirectLinksViewTable' => $baseDir . '/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php', + 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockLinksSetDisplayAsProperty' => $baseDir . '/sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog' => $baseDir . '/sources/Application/UI/Links/Indirect/BlockObjectPickerDialog.php', + 'Combodo\\iTop\\Application\\UI\\Links\\Set\\LinksSetUIBlockFactory' => $baseDir . '/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => $baseDir . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php', 'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => $baseDir . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php', 'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php', @@ -425,12 +434,18 @@ return array( 'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\Router\\Router' => $baseDir . '/sources/Router/Router.php', + 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => $baseDir . '/sources/Service/Base/ObjectRepository.php', + 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => $baseDir . '/sources/Service/Base/iDataPostProcessor.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => $baseDir . '/sources/Application/Service/Events/Description/EventDataDescription.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDescription' => $baseDir . '/sources/Application/Service/Events/Description/EventDescription.php', 'Combodo\\iTop\\Service\\Events\\EventData' => $baseDir . '/sources/Application/Service/Events/EventData.php', 'Combodo\\iTop\\Service\\Events\\EventHelper' => $baseDir . '/sources/Application/Service/Events/EventHelper.php', 'Combodo\\iTop\\Service\\Events\\EventService' => $baseDir . '/sources/Application/Service/Events/EventService.php', 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => $baseDir . '/sources/Application/Service/Events/iEventServiceSetup.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => $baseDir . '/sources/Service/Links/LinkSetDataTransformer.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => $baseDir . '/sources/Service/Links/LinkSetModel.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => $baseDir . '/sources/Service/Links/LinkSetRepository.php', + 'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => $baseDir . '/sources/Service/Links/LinksBulkDataPostProcessor.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php index 5fca5b03b..cc554d8d1 100644 --- a/lib/composer/autoload_real.php +++ b/lib/composer/autoload_real.php @@ -60,16 +60,11 @@ class ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f } } -/** - * @param string $fileIdentifier - * @param string $file - * @return void - */ function composerRequire7f81b4a2a468a061c306af5e447a9a9f($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index d9282aa5c..63b1f42c8 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -619,6 +619,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\Select' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Select/Select.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\SelectOption' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Select/SelectOption.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Select\\SelectOptionUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Select/SelectOptionUIBlockFactory.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AbstractDataProvider' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AjaxDataProvider' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\AjaxDataProviderForOQL' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\SimpleDataProvider' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\DataProvider\\iDataProvider' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\Set' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/Set.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\Set\\SetUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\TextArea' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/TextArea.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/tInputLabel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php', @@ -714,7 +721,9 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Application\\UI\\Links\\Direct\\BlockDirectLinksViewTable' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Direct/BlockDirectLinksViewTable.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockIndirectLinksEditTable' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Indirect/BlockIndirectLinksEditTable.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockIndirectLinksViewTable' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php', + 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockLinksSetDisplayAsProperty' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php', 'Combodo\\iTop\\Application\\UI\\Links\\Indirect\\BlockObjectPickerDialog' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Indirect/BlockObjectPickerDialog.php', + 'Combodo\\iTop\\Application\\UI\\Links\\Set\\LinksSetUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Preferences\\BlockShortcuts\\BlockShortcuts' => __DIR__ . '/../..' . '/sources/Application/UI/Preferences/BlockShortcuts/BlockShortcuts.php', 'Combodo\\iTop\\Application\\UI\\Printable\\BlockPrintHeader\\BlockPrintHeader' => __DIR__ . '/../..' . '/sources/Application/UI/Printable/BlockPrintHeader/BlockPrintHeader.php', 'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php', @@ -790,12 +799,18 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\Router\\Router' => __DIR__ . '/../..' . '/sources/Router/Router.php', + 'Combodo\\iTop\\Service\\Base\\ObjectRepository' => __DIR__ . '/../..' . '/sources/Service/Base/ObjectRepository.php', + 'Combodo\\iTop\\Service\\Base\\iDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Base/iDataPostProcessor.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDataDescription' => __DIR__ . '/../..' . '/sources/Application/Service/Events/Description/EventDataDescription.php', 'Combodo\\iTop\\Service\\Events\\Description\\EventDescription' => __DIR__ . '/../..' . '/sources/Application/Service/Events/Description/EventDescription.php', 'Combodo\\iTop\\Service\\Events\\EventData' => __DIR__ . '/../..' . '/sources/Application/Service/Events/EventData.php', 'Combodo\\iTop\\Service\\Events\\EventHelper' => __DIR__ . '/../..' . '/sources/Application/Service/Events/EventHelper.php', 'Combodo\\iTop\\Service\\Events\\EventService' => __DIR__ . '/../..' . '/sources/Application/Service/Events/EventService.php', 'Combodo\\iTop\\Service\\Events\\iEventServiceSetup' => __DIR__ . '/../..' . '/sources/Application/Service/Events/iEventServiceSetup.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetDataTransformer' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetDataTransformer.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetModel' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetModel.php', + 'Combodo\\iTop\\Service\\Links\\LinkSetRepository' => __DIR__ . '/../..' . '/sources/Service/Links/LinkSetRepository.php', + 'Combodo\\iTop\\Service\\Links\\LinksBulkDataPostProcessor' => __DIR__ . '/../..' . '/sources/Service/Links/LinksBulkDataPostProcessor.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', diff --git a/lib/composer/installed.php b/lib/composer/installed.php index ea28b0295..a06e2ae12 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '1cac1890774b4defa212a142f88348f2f8743f4c', + 'reference' => '9482139b5aec9978f05b1cbb0542b24c18681819', 'name' => 'combodo/itop', 'dev' => true, ), @@ -25,7 +25,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '1cac1890774b4defa212a142f88348f2f8743f4c', + 'reference' => '9482139b5aec9978f05b1cbb0542b24c18681819', 'dev_requirement' => false, ), 'combodo/tcpdf' => array( diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 9dcbc3e4a..e92209f71 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1499,6 +1499,14 @@ EOF; $aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0); $aParameters['duplicates'] = $this->GetPropBoolean($oField, 'duplicates', false); $aParameters['depends_on'] = $sDependencies; + $aParameters['display_style'] = $this->GetPropString($oField, 'display_style'); + if ($sOql = $oField->GetChildText('filter')) { + $sEscapedOql = self::QuoteForPHP($sOql); + $aParameters['allowed_values'] = "new ValueSetObjects($sEscapedOql)"; + } else { + $aParameters['allowed_values'] = 'null'; + } + } elseif ($sAttType == 'AttributeLinkedSet') { @@ -1506,6 +1514,7 @@ EOF; $aParameters['ext_key_to_me'] = $this->GetMandatoryPropString($oField, 'ext_key_to_me'); $aParameters['count_min'] = $this->GetPropNumber($oField, 'count_min', 0); $aParameters['count_max'] = $this->GetPropNumber($oField, 'count_max', 0); + $aParameters['display_style'] = $this->GetPropString($oField, 'display_style'); $sEditMode = $oField->GetChildText('edit_mode'); if (!is_null($sEditMode)) { diff --git a/setup/itop_design.xsd b/setup/itop_design.xsd index de24c1c9c..146f2af0a 100644 --- a/setup/itop_design.xsd +++ b/setup/itop_design.xsd @@ -384,6 +384,7 @@ + diff --git a/sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php new file mode 100644 index 000000000..c8886bd51 --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AbstractDataProvider.php @@ -0,0 +1,160 @@ +Init(); + } + + /** + * Initialization. + * + * @return void + */ + private function Init() + { + $this->sDataLabelField = 'label'; + $this->sDataValueField = 'value'; + $this->aDataSearchFields = ['search']; + $this->sGroupField = null; + $this->sTooltipField = 'label'; + } + + /** @inheritDoc */ + public function GetDataValueField(): string + { + return $this->sDataValueField; + } + + /** @inheritDoc */ + public function SetDataValueField(string $sField): AbstractDataProvider + { + $this->sDataValueField = $sField; + + return $this; + } + + /** @inheritDoc */ + public function GetDataLabelField(): string + { + return $this->sDataLabelField; + } + + /** @inheritDoc */ + public function SetDataLabelField(string $sField): AbstractDataProvider + { + $this->sDataLabelField = $sField; + + return $this; + } + + /** @inheritDoc */ + public function GetDataSearchFields(): array + { + return $this->aDataSearchFields; + } + + /** @inheritDoc */ + public function SetDataSearchFields(array $aFields): AbstractDataProvider + { + $this->aDataSearchFields = $aFields; + + return $this; + } + + /** @inheritDoc */ + public function GetGroupField(): ?string + { + return $this->sGroupField; + } + + /** @inheritDoc */ + public function SetGroupField(string $sField): iDataProvider + { + $this->sGroupField = $sField; + + return $this; + } + + /** @inheritDoc */ + public function GetTooltipField(): ?string + { + return $this->sTooltipField; + } + + /** @inheritDoc */ + public function SetTooltipField(string $sField): iDataProvider + { + $this->sTooltipField = $sField; + + return $this; + } + + /** + * IsAjaxProviderType. + * + * @return bool + */ + public function IsAjaxProviderType(): bool + { + return $this->GetType() === iDataProvider::TYPE_AJAX_PROVIDER; + } + + /** + * IsSimpleProviderType. + * + * @return bool + */ + public function IsSimpleProviderType(): bool + { + return $this->GetType() === iDataProvider::TYPE_SIMPLE_PROVIDER; + } + + +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php new file mode 100644 index 000000000..503d0741c --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProvider.php @@ -0,0 +1,169 @@ +sRoute = $sRoute; + $this->aParams = $aParams; + $this->aPostParams = $aPostParams; + } + + /** @inheritDoc */ + public function GetType(): string + { + return iDataProvider::TYPE_AJAX_PROVIDER; + } + + /** + * SetParam. + * + * @param string $sName + * @param string $sValue + * + * @return $this + */ + public function SetParam(string $sName, string $sValue): AjaxDataProvider + { + $this->aParams[$sName] = $sValue; + + return $this; + } + + /** + * GetParam. + * + * @param string $sName + * + * @return string + */ + public function GetParam(string $sName): string + { + return $this->aParams[$sName]; + } + + /** + * GetParams. + * + * @return array + */ + public function GetParams(): array + { + return $this->aParams; + } + + /** + * GetParamsAsQueryString. + * + * @return string + */ + public function GetParamsAsQueryString(): string + { + $aFlattened = $this->aParams; + array_walk($aFlattened, function (&$sValue, $key) { + $sValue = "{$key}={$sValue}"; + }); + + return '&'.implode('&', $aFlattened); + } + + /** + * GetPostParamsAsJsonString. + * + * @return string + */ + public function GetPostParamsAsJsonString(): string + { + return json_encode($this->aPostParams); + } + + /** + * SetPostParam. + * + * @param string $sName + * @param $oValue + * + * @return $this + */ + public function SetPostParam(string $sName, $oValue): AjaxDataProvider + { + $this->aPostParams[$sName] = $oValue; + + return $this; + } + + /** + * GetRoute. + * + * @return void + */ + public function GetRoute(): string + { + return $this->sRoute; + } + + /** + * Return maximum results count. + * + * @return int + */ + public function GetMaxResults(): int + { + return $this->iMaxResults; + } + +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php new file mode 100644 index 000000000..66ed71b39 --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/DataProvider/AjaxDataProviderForOql.php @@ -0,0 +1,65 @@ + $sObjectClass, + 'oql' => $sOql, + 'fields_to_load' => json_encode($aFieldsToLoad), + ], [ + 'this_object_data' => $sWizardHelperJsVarName != null ? "EVAL_JAVASCRIPT{{$sWizardHelperJsVarName}.UpdateWizardToJSON();}" : "", + ]); + + // Initialization + $this->Init(); + } + + /** + * Initialization. + * + * @return void + */ + private function Init() + { + $this->SetDataLabelField('friendlyname') + ->SetDataValueField('key') + ->SetDataSearchFields(['friendlyname', 'additional_field']); + } + +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php b/sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php new file mode 100644 index 000000000..a52347d82 --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/DataProvider/SimpleDataProvider.php @@ -0,0 +1,95 @@ +SetOptions($aOptions); + } + + /** @inheritDoc */ + public function GetType(): string + { + return iDataProvider::TYPE_SIMPLE_PROVIDER; + } + + /** @inheritDoc */ + public function SetOptions(array $aOptions): SimpleDataProvider + { + $this->aOptions = $aOptions; + + return $this; + } + + /** @inheritDoc */ + public function SetOption(string $sKey, string $sValue): SimpleDataProvider + { + $this->aOptions[$sKey] = $sValue; + + return $this; + } + + /** @inheritDoc */ + public function GetOptions(): array + { + return $this->aOptions; + } + + /** + * GetOptionsGroups. + * + * @return array + */ + public function GetOptionsGroups(): array + { + $aGroups = []; + if ($this->GetGroupField() != null) { + foreach ($this->GetOptions() as $aOption) { + if (array_key_exists($this->GetGroupField(), $aOption)) { + $aGroups[$aOption[$this->GetGroupField()]] = [ + 'label' => $aOption[$this->GetGroupField()], + 'value' => $aOption[$this->GetGroupField()], + ]; + } + } + } + + return array_values($aGroups); + } +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php b/sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php new file mode 100644 index 000000000..9983ae1a9 --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/DataProvider/iDataProvider.php @@ -0,0 +1,145 @@ +Init(); + } + + /** + * Initialization. + * + * @return void + */ + private function Init() + { + $this->SetValue('[]'); + // @todo BDA placeholder depending on autocomplete activation (search...., click to add...) + $this->SetPlaceholder(Dict::S('Core:AttributeSet:placeholder')); + $this->iMaxItems = null; + $this->iMaxOptions = null; + $this->bHasRemoveItemButton = true; + $this->bHasAddOptionButton = false; + $this->sAddButtonTitle = Dict::S('UI:Button:Create'); + $this->bIsPreloadEnabled = false; + $this->sTemplateOptions = null; + $this->sTemplateItems = null; + $this->bIsMultiValuesSynthesis = false; + $this->bHasError = false; + } + + /** + * SetMaxItems. + * + * @param int|null $iMaxItems + * + * @return $this + */ + public function SetMaxItems(?int $iMaxItems): Set + { + $this->iMaxItems = $iMaxItems; + + return $this; + } + + /** + * GetMaxItems. + * + * @return int|null + */ + public function GetMaxItems(): ?int + { + return $this->iMaxItems; + } + + /** + * SetMaxOptions. + * + * @param int|null $iMaxOptions + * + * @return $this + */ + public function SetMaxOptions(?int $iMaxOptions): Set + { + $this->iMaxOptions = $iMaxOptions; + + return $this; + } + + /** + * GetMaxOptions. + * + * @return int|null + */ + public function GetMaxOptions(): ?int + { + return $this->iMaxOptions; + } + + /** + * SetHasRemoveItemButton. + * + * @param bool $bHasRemoveItemButton + * + * @return $this + */ + public function SetHasRemoveItemButton(bool $bHasRemoveItemButton): Set + { + $this->bHasRemoveItemButton = $bHasRemoveItemButton; + + return $this; + } + + /** + * HasRemoveItemButton. + * + * @return bool + */ + public function HasRemoveItemButton(): bool + { + return $this->bHasRemoveItemButton; + } + + /** + * SetHasAddOptionButton. + * + * @param bool $bHasAddOptionButton + * + * @return $this + */ + public function SetHasAddOptionButton(bool $bHasAddOptionButton): Set + { + $this->bHasAddOptionButton = $bHasAddOptionButton; + + return $this; + } + + /** + * HasAddOptionButton. + * + * @return bool + */ + public function HasAddOptionButton(): bool + { + return $this->bHasAddOptionButton; + } + + /** + * GetAddButtonTitle. + * + * @return string + */ + public function GetAddButtonTitle(): string + { + return $this->sAddButtonTitle; + } + + /** + * SetAddButtonTitle. + * + * @param string $sTitle + * + * @return $this + */ + public function SetAddButtonTitle(string $sTitle): Set + { + $this->sAddButtonTitle = $sTitle; + + return $this; + } + + /** + * SetPreloadEnabled. + * + * @param bool $bEnabled + * + * @return $this + */ + public function SetPreloadEnabled(bool $bEnabled): Set + { + $this->bIsPreloadEnabled = $bEnabled; + + return $this; + } + + /** + * IsPreloadEnabled. + * + * @return bool + */ + public function IsPreloadEnabled(): bool + { + return $this->bIsPreloadEnabled; + } + + /** + * SetOptionsTemplate. + * + * @param string $sTemplate + * + * @return $this + */ + public function SetOptionsTemplate(string $sTemplate): Set + { + $this->sTemplateOptions = $sTemplate; + + return $this; + } + + /** + * Return options template. + * + * @return string + */ + public function GetOptionsTemplate(): ?string + { + return $this->sTemplateOptions; + } + + /** + * HasOptionsTemplate. + * + * @return bool + */ + public function HasOptionsTemplate(): bool + { + return $this->sTemplateOptions != null; + } + + /** + * SetItemsTemplate. + * + * @param string $sTemplate + * + * @return $this + */ + public function SetItemsTemplate(string $sTemplate): Set + { + $this->sTemplateItems = $sTemplate; + + return $this; + } + + /** + * Return items template. + * + * @return string + */ + public function GetItemsTemplate(): ?string + { + return $this->sTemplateItems; + } + + /** + * HasItemsTemplate. + * + * @return bool + */ + public function HasItemsTemplate(): bool + { + return $this->sTemplateItems != null; + } + + /** + * SetDataProvider. + * + * @param \Combodo\iTop\Application\UI\Base\Component\Input\Set\DataProvider\iDataProvider $oDataProvider + * + * @return $this + */ + public function SetDataProvider(iDataProvider $oDataProvider): Set + { + $this->oDataProvider = $oDataProvider; + + return $this; + } + + /** + * Get data provider. + * + * @return iDataProvider + */ + public function GetDataProvider(): iDataProvider + { + return $this->oDataProvider; + } + + /** + * SetIsMultiValuesSynthesis. + * + * @param bool $bIsMultiValuesSynthesis + * + * @return $this + */ + public function SetIsMultiValuesSynthesis(bool $bIsMultiValuesSynthesis): Set + { + $this->bIsMultiValuesSynthesis = $bIsMultiValuesSynthesis; + + return $this; + } + + /** + * IsMultiValuesSynthesis. + * + * @return bool + */ + public function IsMultiValuesSynthesis(): bool + { + return $this->bIsMultiValuesSynthesis; + } + + /** + * SetHasError. + * + * @param $bHasError + * + * @return $this + */ + public function SetHasError($bHasError): Set + { + $this->bHasError = $bHasError; + + return $this; + } + + /** + * HasError. + * + * @return bool + */ + public function HasError(): bool + { + return $this->bHasError; + } +} \ No newline at end of file diff --git a/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php b/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php new file mode 100644 index 000000000..70cb22eb8 --- /dev/null +++ b/sources/Application/UI/Base/Component/Input/Set/SetUIBlockFactory.php @@ -0,0 +1,154 @@ +SetDataLabelField($sLabelFields) + ->SetDataValueField($sValueField) + ->SetDataSearchFields($aSearchFields) + ->SetTooltipField($sLabelFields); + if ($sGroupField != null) { + $oDataProvider->SetGroupField($sGroupField); + } + $oSetUIBlock->SetDataProvider($oDataProvider); + + return $oSetUIBlock; + } + + /** + * MakeForAjax. + * + * Create a dynamic set base on options provided by ajax call. + * Options array must contain label, value and search string for each option. + * Keys for each entry must be provided but can be the same. + * If a group field is provided, options will be grouped according to this setting. + * + * @param string $sId Block identifier + * @param string $sAjaxRoute Ajax route @see \Combodo\iTop\Router\Router + * @param array $aAjaxRouteParams Url query parameters + * @param string $sLabelFields Field used for label + * @param string $sValueField Field used for value + * @param array $aSearchFields Fields used for search + * @param string|null $sGroupField Field used for grouping + * + * @return \Combodo\iTop\Application\UI\Base\Component\Input\Set\Set + */ + public static function MakeForAjax(string $sId, string $sAjaxRoute, array $aAjaxRouteParams, string $sLabelFields, string $sValueField, array $aSearchFields, ?string $sGroupField = null): Set + { + // Create set ui block + $oSetUIBlock = new Set($sId); + + // Ajax data provider + $oDataProvider = new AjaxDataProvider($sAjaxRoute, $aAjaxRouteParams); + $oDataProvider + ->SetDataLabelField($sLabelFields) + ->SetDataValueField($sValueField) + ->SetDataSearchFields($aSearchFields) + ->SetTooltipField($sLabelFields); + if ($sGroupField != null) { + $oDataProvider->SetGroupField($sGroupField); + } + $oSetUIBlock->SetDataProvider($oDataProvider); + + return $oSetUIBlock; + } + + /** + * MakeForOQL. + * + * Create a oql set base on options provided by OQL call. + * Options array must contain label, value and search string for each option. + * Keys for each entry must be provided but can be the same. + * If a group field is provided, options will be grouped according to this setting. + * Default fields are loaded but you can request more. + * + * @param string $sId Block identifier + * @param string $sObjectClass Object class + * @param string $sOql OQL to query objects + * @param string|null $sWizardHelperJsVarName Wizard helper name + * @param array $aFieldsToLoad Additional fields to load on objects + * @param string|null $sGroupField Field used for grouping + * + * @return \Combodo\iTop\Application\UI\Base\Component\Input\Set\Set + */ + public static function MakeForOQL(string $sId, string $sObjectClass, string $sOql, string $sWizardHelperJsVarName = null, array $aFieldsToLoad = [], ?string $sGroupField = null): Set + { + // Create set ui block + $oSetUIBlock = new Set($sId); + + // Renderers + $oSetUIBlock->SetOptionsTemplate('application/object/set/option_renderer.html.twig'); + $oSetUIBlock->SetItemsTemplate('application/object/set/item_renderer.html.twig'); + + // OQL data provider + $oDataProvider = new AjaxDataProviderForOQL($sObjectClass, $sOql, $sWizardHelperJsVarName, $aFieldsToLoad); + if ($sGroupField != null) { + $oDataProvider->SetGroupField($sGroupField); + } + $oDataProvider->SetTooltipField('full_description'); + $oSetUIBlock->SetDataProvider($oDataProvider); + + return $oSetUIBlock; + } +} \ No newline at end of file diff --git a/sources/Application/UI/Links/AbstractBlockLinksViewTable.php b/sources/Application/UI/Links/AbstractBlockLinksViewTable.php index d15ad92f8..5911e5989 100644 --- a/sources/Application/UI/Links/AbstractBlockLinksViewTable.php +++ b/sources/Application/UI/Links/AbstractBlockLinksViewTable.php @@ -6,9 +6,21 @@ namespace Combodo\iTop\Application\UI\Links; +use ApplicationException; +use ArchivedObjectException; +use AttributeLinkedSet; use Combodo\iTop\Application\UI\Base\Component\MedallionIcon\MedallionIcon; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock; +use CoreException; +use CoreWarning; +use DBObject; +use DictExceptionMissingString; +use DisplayBlock; +use Exception; use MetaModel; +use MySQLException; +use Utils; +use WebPage; /** * Class AbstractBlockLinksViewTable @@ -27,8 +39,8 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock 'js/wizardhelper.js', ]; - /** @var \DBObject $oDbObject db object witch link set belongs to */ - protected \DBObject $oDbObject; + /** @var DBObject $oDbObject db object witch link set belongs to */ + protected DBObject $oDbObject; /** @var string $sObjectClass db object class name */ protected string $sObjectClass; @@ -36,27 +48,27 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock /** @var string $sAttCode db object link set attribute code */ protected string $sAttCode; - /** @var \AttributeLinkedSet $oAttDef attribute link set */ - protected \AttributeLinkedSet $oAttDef; + /** @var AttributeLinkedSet $oAttDef attribute link set */ + protected AttributeLinkedSet $oAttDef; /** @var string $sTargetClass links target classname */ protected string $sTargetClass; - + protected string $sTableId; /** * Constructor. * - * @param \WebPage $oPage - * @param \DBObject $oDbObject + * @param WebPage $oPage + * @param DBObject $oDbObject * @param string $sObjectClass * @param string $sAttCode - * @param \AttributeLinkedSet $oAttDef + * @param AttributeLinkedSet $oAttDef * - * @throws \CoreException - * @throws \Exception + * @throws CoreException + * @throws Exception */ - public function __construct(\WebPage $oPage, \DBObject $oDbObject, string $sObjectClass, string $sAttCode, \AttributeLinkedSet $oAttDef) + public function __construct(WebPage $oPage, DBObject $oDbObject, string $sObjectClass, string $sAttCode, AttributeLinkedSet $oAttDef) { parent::__construct('', ["ibo-block-links-table"]); @@ -78,7 +90,7 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * Init. * * @return void - * @throws \Exception + * @throws Exception */ private function Init() { @@ -89,12 +101,12 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * Initialize UI. * * @return void - * @throws \CoreException + * @throws CoreException */ - private function InitUI(\WebPage $oPage) + private function InitUI(WebPage $oPage) { // header - $this->InitHeader();; + $this->InitHeader(); // Table $this->InitTable($oPage); @@ -104,7 +116,8 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * InitHeader. * * @return void - * @throws \CoreException + * @throws CoreException + * @throws \Exception */ private function InitHeader() { @@ -117,21 +130,21 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock /** * InitTable. * - * @param \WebPage $oPage + * @param WebPage $oPage * * @return void - * @throws \ApplicationException - * @throws \ArchivedObjectException - * @throws \CoreException - * @throws \CoreWarning - * @throws \DictExceptionMissingString - * @throws \MySQLException + * @throws ApplicationException + * @throws ArchivedObjectException + * @throws CoreException + * @throws CoreWarning + * @throws DictExceptionMissingString + * @throws MySQLException */ - private function InitTable(\WebPage $oPage) + private function InitTable(WebPage $oPage) { // retrieve db object set $oOrmLinkSet = $this->oDbObject->Get($this->sAttCode); - $oLinkSet = $oOrmLinkSet->ToDBObjectSet(\utils::ShowObsoleteData()); + $oLinkSet = $oOrmLinkSet->ToDBObjectSet(utils::ShowObsoleteData()); // add list block $oBlock = new \DisplayBlock($oLinkSet->GetFilter(), 'list', false); @@ -163,11 +176,11 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * * Provide parameters for display block as list. * - * @see \DisplayBlock::RenderList + * @see DisplayBlock::RenderList * * @return array - * @throws \ArchivedObjectException - * @throws \CoreException + * @throws ArchivedObjectException + * @throws CoreException */ abstract function GetExtraParam(): array; @@ -178,7 +191,7 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * * @see \Combodo\iTop\Application\UI\Base\Component\DataTable\tTableRowActions * - * @return \string[][] + * @return string[][] */ abstract function GetRowActions(): array; @@ -188,12 +201,11 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * Return link set target class. * * @return string - * @throws \Exception + * @throws Exception */ abstract function GetTargetClass(): string; - /** * @return string */ @@ -201,5 +213,5 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock { return $this->sAttCode; } - + } \ No newline at end of file diff --git a/sources/Application/UI/Links/Direct/BlockDirectLinksEditTable.php b/sources/Application/UI/Links/Direct/BlockDirectLinksEditTable.php index 27a0ec819..92abad62e 100644 --- a/sources/Application/UI/Links/Direct/BlockDirectLinksEditTable.php +++ b/sources/Application/UI/Links/Direct/BlockDirectLinksEditTable.php @@ -127,7 +127,7 @@ class BlockDirectLinksEditTable extends UIContentBlock * * @return void */ - public function InitTable(\WebPage $oPage, \DBObjectSet $oValue, string $sFormPrefix) + public function InitTable(\WebPage $oPage, $oValue, string $sFormPrefix) { /** @todo fields initialization */ $this->sInputName = $sFormPrefix.'attr_'.$this->oUILinksDirectWidget->GetAttCode(); @@ -193,26 +193,6 @@ class BlockDirectLinksEditTable extends UIContentBlock $aRow['form::select'] = ''; foreach ($this->oUILinksDirectWidget->GetZList() as $sLinkedAttCode) { $aRow[$sLinkedAttCode] = $oLinkObj->GetAsHTML($sLinkedAttCode); - - // tentative d'ajout des attributs en édition -// $sValue = $oLinkObj->Get($sLinkedAttCode); -// $sDisplayValue = $oLinkObj->GetEditValue($sLinkedAttCode); -// $oAttDef = MetaModel::GetAttributeDef($this->oUILinksDirectWidget->GetLinkedClass(), $sLinkedAttCode); -// -// $aRow[$sLinkedAttCode] = '
' -// .\cmdbAbstractObject::GetFormElementForField( -// $oPage, -// $this->oUILinksDirectWidget->GetLinkedClass(), -// $sLinkedAttCode, -// $oAttDef, -// $sValue, -// $sDisplayValue, -// $this->GetFieldId($oValue, $sLinkedAttCode), -// ']', -// 0, -// [] -// ) -// .'
'; } $aRows[] = $aRow; } diff --git a/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php b/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php index 9e3df641e..bed904846 100644 --- a/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php +++ b/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php @@ -19,7 +19,8 @@ use PHPUnit\Exception; */ class BlockIndirectLinksViewTable extends AbstractBlockLinksViewTable { - public const BLOCK_CODE = 'ibo-block-indirect-links-view-table'; + public const BLOCK_CODE = 'ibo-block-indirect-links-view-table'; + public const REQUIRES_ANCESTORS_DEFAULT_JS_FILES = true; /** @inheritdoc */ public function GetTargetClass(): string diff --git a/sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php b/sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php new file mode 100644 index 000000000..9c97f8efe --- /dev/null +++ b/sources/Application/UI/Links/Set/BlockLinksSetDisplayAsProperty.php @@ -0,0 +1,161 @@ +oAttribute = $oAttribute; + $this->oValue = $oValue; + + // Initialization + $this->Init(); + + // UI Initialization + $this->InitUI(); + } + + /** + * Initialization. + * + * @return void + * @throws \Twig\Error\LoaderError + */ + private function Init() + { + // Link set model properties + $this->sTargetClass = LinkSetModel::GetTargetClass($this->oAttribute); + $sTargetField = LinkSetModel::GetTargetField($this->oAttribute); + + // Get objects from linked data + $this->aObjectsData = LinkSetRepository::LinksDbSetToTargetObjectArray($this->oValue, $this->sTargetClass, $sTargetField); + + // Twig environment + $this->oTwigEnv = TwigHelper::GetTwigEnvironment(TwigHelper::ENUM_TEMPLATES_BASE_PATH_BACKOFFICE); + + $oAppContext = new ApplicationContext(); + $this->sAppContext = $oAppContext->GetForLink(); + $this->sUIPage = cmdbAbstractObject::ComputeStandardUIPage($this->sTargetClass); + } + + /** + * UI Initialization. + * + * @return void + * @throws \Exception + */ + private function InitUI() + { + // Error handling + if ($this->aObjectsData === null) { + $sMessage = "Error while displaying attribute {$this->oAttribute->GetCode()}"; + $this->AddSubBlock(HtmlFactory::MakeHtmlContent($sMessage)); + + return; + } + + // Container + $sHtml = ''; + + // Iterate throw data... + foreach ($this->aObjectsData as $aItem) { + + // Ignore obsolete data + if (!utils::ShowObsoleteData() && $aItem['obsolescence_flag']) { + continue; + } + + // Generate template + $sTemplate = TwigHelper::RenderTemplate($this->oTwigEnv, $aItem, 'application/object/set/set_renderer'); + + // Friendly name + $sFriendlyNameForHtml = utils::HtmlEntities($aItem['friendlyname']); + + // Append value + $sHtml .= 'GenerateLinkUrl($aItem['key']).' class="attribute-set-item" data-label="'.$sFriendlyNameForHtml.'" data-tooltip-content="'.$sFriendlyNameForHtml.'">'.$sTemplate.''; + } + + // Close container + $sHtml .= ''; + + // Make html block + $this->AddSubBlock(HtmlFactory::MakeHtmlContent($sHtml)); + } + + /** + * GenerateLinkUrl. + * + * @param $id + * + * @return string + * @throws \Exception + */ + private function GenerateLinkUrl($id): string + { + return ' href="' + .utils::GetAbsoluteUrlAppRoot() + ."pages/$this->sUIPage?operation=details&class=$this->sTargetClass&id=$id&$this->sAppContext" + .'" target="_blank"'; + } +} \ No newline at end of file diff --git a/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php b/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php new file mode 100644 index 000000000..dd1f284f0 --- /dev/null +++ b/sources/Application/UI/Links/Set/LinksSetUIBlockFactory.php @@ -0,0 +1,118 @@ +GetValuesDef()->GetFilterExpression(), $sWizardHelperJsVarName); + + // Current value + $aCurrentValues = LinkSetDataTransformer::Decode($oDbObjectSet, $sTargetClass, $sTargetField); + + // Initial options data + $aInitialOptions = LinkSetRepository::LinksDbSetToTargetObjectArray($oDbObjectSet, $sTargetClass, $sTargetField); + if ($aInitialOptions !== null) { + $oSetUIBlock->GetDataProvider()->SetOptions($aInitialOptions); + // Set value + $oSetUIBlock->SetValue(json_encode($aCurrentValues)); + } else { + $oSetUIBlock->SetHasError(true); + } + + return $oSetUIBlock; + } + + /** + * Make a link set block for bulk modify. + * + * @param string $sId Block identifier + * @param AttributeLinkedSet $oAttDef Link set attribute definition + * @param iDBObjectSetIterator $oDbObjectSet Link set value + * @param string $sWizardHelperJsVarName Wizard helper name + * @param array $aBulkContext + * + * @return \Combodo\iTop\Application\UI\Base\Component\Input\Set\Set + */ + public static function MakeForBulkLinkSet(string $sId, AttributeLinkedSet $oAttDef, iDBObjectSetIterator $oDbObjectSet, string $sWizardHelperJsVarName, array $aBulkContext): Set + { + $oSetUIBlock = self::MakeForLinkSet($sId, $oAttDef, $oDbObjectSet, $sWizardHelperJsVarName); + + // Bulk modify specific + $oSetUIBlock->GetDataProvider()->SetGroupField('group'); + $oSetUIBlock->SetIsMultiValuesSynthesis(true); + + // Data post processing + $aBinderSettings = [ + 'bulk_oql' => $aBulkContext['oql'], + 'link_class' => LinkSetModel::GetLinkedClass($oAttDef), + 'target_field' => LinkSetModel::GetTargetField($oAttDef), + 'origin_field' => $oAttDef->GetExtKeyToMe(), + ]; + + // Initial options + $aOptions = $oSetUIBlock->GetDataProvider()->GetOptions(); + $aOptions = LinksBulkDataPostProcessor::Execute($aOptions, $aBinderSettings); + $oSetUIBlock->GetDataProvider()->SetOptions($aOptions); + + // Data provider post processor + /** @var \Combodo\iTop\Application\UI\Base\Component\Input\Set\DataProvider\AjaxDataProvider $oDataProvider */ + $oDataProvider = $oSetUIBlock->GetDataProvider(); + $oDataProvider->SetPostParam('data_post_processor', [ + 'class_name' => addslashes(LinksBulkDataPostProcessor::class), + 'settings' => $aBinderSettings, + ]); + + return $oSetUIBlock; + } +} \ No newline at end of file diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php index e9706641b..70db72788 100644 --- a/sources/Controller/Base/Layout/ObjectController.php +++ b/sources/Controller/Base/Layout/ObjectController.php @@ -14,6 +14,7 @@ use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; use Combodo\iTop\Controller\AbstractController; +use Combodo\iTop\Service\Base\ObjectRepository; use CoreCannotSaveObjectException; use DeleteException; use Dict; @@ -543,4 +544,44 @@ JS; 'js/jquery.blockUI.js', ]; } + + /** + * OperationSearch. + * + * Search objects via an oql and a friendly name search string + * + * @return JsonPage + */ + public function OperationSearch(): JsonPage + { + $oPage = new JsonPage(); + + // Retrieve query params + $sObjectClass = utils::ReadParam('object_class', '', false, utils::ENUM_SANITIZATION_FILTER_STRING); + $sOql = utils::ReadParam('oql', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + $aFieldsToLoad = json_decode(utils::ReadParam('fields_to_load', '', false, utils::ENUM_SANITIZATION_FILTER_STRING)); + $sSearch = utils::ReadParam('search', '', false, utils::ENUM_SANITIZATION_FILTER_STRING); + + // Retrieve this reference object (for OQL) + $sThisObjectData = utils::ReadPostedParam('this_object_data', null, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + $oThisObj = ObjectRepository::GetObjectFromWizardHelperData($sThisObjectData); + + // Retrieve data post processor + $aDataProcessor = utils::ReadParam('data_post_processor', null, false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); + + // Search objects + $aResult = ObjectRepository::SearchFromOql($sObjectClass, $aFieldsToLoad, $sOql, $sSearch, $oThisObj); + + // Data post processor + // Note: Data post processor allow you to perform actions on search result (compute object result statistics, add others information...). + if ($aResult !== null && $aDataProcessor !== null) { + $aResult = call_user_func(array($aDataProcessor['class_name'], 'Execute'), $aResult, $aDataProcessor['settings']); + } + + return $oPage->SetData([ + 'search_data' => $aResult, + 'success' => $aResult !== null, + ]); + } + } \ No newline at end of file diff --git a/sources/Controller/Links/LinksetController.php b/sources/Controller/Links/LinksetController.php index 3ad0fd443..a98e1b4ef 100644 --- a/sources/Controller/Links/LinksetController.php +++ b/sources/Controller/Links/LinksetController.php @@ -10,9 +10,10 @@ use AjaxPage; use cmdbAbstractObject; use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory; use Combodo\iTop\Controller\AbstractController; +use Exception; +use JsonPage; use CoreException; use DBObject; -use JsonPage; use MetaModel; use UserRights; use utils; @@ -58,7 +59,7 @@ class LinkSetController extends AbstractController $sErrorMessage = json_encode($oDeletionPlan->GetIssues()); } } - catch (\Exception $e) { + catch (Exception $e) { $sErrorMessage = $e->getMessage(); } } else { @@ -102,7 +103,7 @@ class LinkSetController extends AbstractController $oLinkedObject->DBWrite(); $bOperationSuccess = true; } - catch (\Exception $e) { + catch (Exception $e) { $sErrorMessage = $e->getMessage(); } } else { diff --git a/sources/Service/Base/ObjectRepository.php b/sources/Service/Base/ObjectRepository.php new file mode 100644 index 000000000..ae98a7688 --- /dev/null +++ b/sources/Service/Base/ObjectRepository.php @@ -0,0 +1,263 @@ +SetShowObsoleteData(utils::ShowObsoleteData()); + + // Add a friendly name search condition + $oDbObjectSearch->AddCondition('friendlyname', $sSearch, 'Contains'); + + // Create db object set + $oSet = new DBObjectSet($oDbObjectSearch); + + // Transform set to array + $aResult = ObjectRepository::DBSetToObjectArray($oSet, $sObjectClass, $aFieldsToLoad); + + // Handle max results for autocomplete + if (Utils::IsNullOrEmptyString($sSearch) + && count($aResult) > MetaModel::GetConfig()->Get('max_autocomplete_results')) { + return []; + } + + return $aResult; + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + + return null; + } + } + + /** + * SearchFromOql. + * + * @param string $sObjectClass Object class to search + * @param array $aFieldsToLoad Additional fields to load + * @param string $sOql Oql expression + * @param string $sSearch Friendly name search string + * @param DBObject|null $oThisObject This object reference for oql + * + * @return array|null + */ + static public function SearchFromOql(string $sObjectClass, array $aFieldsToLoad, string $sOql, string $sSearch, DBObject $oThisObject = null): ?array + { + try { + + // Create db search + $oDbObjectSearch = DBSearch::FromOQL($sOql); + $oDbObjectSearch->SetShowObsoleteData(utils::ShowObsoleteData()); + $oDbObjectSearch->AddCondition('friendlyname', $sSearch, 'Contains'); + + // Create db set from db search + $oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], ['this' => $oThisObject]); + + // return object array + return ObjectRepository::DBSetToObjectArray($oDbObjectSet, $sObjectClass, $aFieldsToLoad); + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + + return null; + } + } + + /** + * DBSetToObjectArray. + * + * @param iDBObjectSetIterator $oDbObjectSet Db object set + * @param string $sObjectClass Object class + * @param array $aFieldsToLoad Additional fields to load + * + * @return array + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \DictExceptionMissingString + */ + static private function DBSetToObjectArray(iDBObjectSetIterator $oDbObjectSet, string $sObjectClass, array $aFieldsToLoad): array + { + // Retrieve friendly name complementary specification + $aComplementAttributeSpec = MetaModel::GetNameSpec($sObjectClass, FriendlyNameType::COMPLEMENTARY); + + // Retrieve image attribute code + $sObjectImageAttCode = MetaModel::GetImageAttributeCode($sObjectClass); + + // Prepare fields to load + $aDefaultFieldsToLoad = ObjectRepository::GetDefaultFieldsToLoad($aComplementAttributeSpec, $sObjectImageAttCode); + $aFieldsToLoad = array_merge($aDefaultFieldsToLoad, $aFieldsToLoad); + + // Optimize columns load + $oDbObjectSet->OptimizeColumnLoad([ + $sObjectClass => $aFieldsToLoad, + ]); + + // Prepare result + $aResult = []; + + // Iterate throw objects... + $oDbObjectSet->Rewind(); + while ($oObject = $oDbObjectSet->Fetch()) { + + // Prepare objet data + $aObjectData = []; + + // Object key + $aObjectData['key'] = $oObject->GetKey(); + + // Fill loaded columns... + foreach ($aFieldsToLoad as $sField) { + $aObjectData[$sField] = $oObject->Get($sField); + } + + // Compute others data + $aResult[] = ObjectRepository::ComputeOthersData($oObject, $sObjectClass, $aObjectData, $aComplementAttributeSpec, $sObjectImageAttCode); + } + + return $aResult; + } + + /** + * GetDefaultFieldsToLoad. + * + * Return attributes to load for any objects. + * + * @param array $aComplementAttributeSpec Friendly name complementary spec + * @param string $sObjectImageAttCode Image attribute code + * + * @return mixed + */ + static public function GetDefaultFieldsToLoad(array $aComplementAttributeSpec, string $sObjectImageAttCode) + { + // Friendly name complementary fields + $aFieldsToLoad = $aComplementAttributeSpec[1]; + + // Image attribute + if (!empty($sObjectImageAttCode)) { + $aFieldsToLoad[] = $sObjectImageAttCode; + } + + // Add friendly name + $aFieldsToLoad[] = 'friendlyname'; + + return $aFieldsToLoad; + } + + /** + * ComputeOthersData. + * + * @param DBObject $oDbObject Db object + * @param string $sClass Object class + * @param array $aData Object data to fill + * @param array $aComplementAttributeSpec Friendly name complementary spec + * @param string $sObjectImageAttCode Image attribute code + * + * @return array + */ + static public function ComputeOthersData(DBObject $oDbObject, string $sClass, array $aData, array $aComplementAttributeSpec, string $sObjectImageAttCode): array + { + try { + + // Obsolescence flag + $aData['obsolescence_flag'] = $oDbObject->IsObsolete(); + + // Additional fields + if (count($aComplementAttributeSpec[1]) > 0) { + $aArguments = []; + foreach ($aComplementAttributeSpec[1] as $sAdditionalField) { + $aArguments[] = $oDbObject->Get($sAdditionalField); + } + $aData['additional_field'] = utils::HtmlEntities(vsprintf($aComplementAttributeSpec[0], $aArguments)); + $aData['full_description'] = "{$aData['friendlyname']}
{$aData['additional_field']}"; + } else { + $aData['full_description'] = $aData['friendlyname']; + } + + // Image + if (!empty($sObjectImageAttCode)) { + $aData['has_image'] = true; + /** @var \ormDocument $oImage */ + $oImage = $oDbObject->Get($sObjectImageAttCode); + if (!$oImage->IsEmpty()) { + $aData['picture_url'] = "url('{$oImage->GetDisplayURL($sClass, $oDbObject->GetKey(), $sObjectImageAttCode)}')"; + $aData['initials'] = ''; + } else { + $aData['initials'] = utils::FormatInitialsForMedallion(utils::ToAcronym($oDbObject->Get('friendlyname'))); + } + } + + return $aData; + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + + return $aData; + } + } + + /** + * GetObjectFromWizardHelperData + * + * @param string $sData + * + * @return DBObject|null + */ + public static function GetObjectFromWizardHelperData(string $sData): ?DBObject + { + try { + $oThisObj = null; + if ($sData != null) { + $oWizardHelper = WizardHelper::FromJSON($sData); + $oThisObj = $oWizardHelper->GetTargetObject(); + } + + return $oThisObj; + } + catch (Exception $e) { + return null; + } + } + +} \ No newline at end of file diff --git a/sources/Service/Base/iDataPostProcessor.php b/sources/Service/Base/iDataPostProcessor.php new file mode 100644 index 000000000..f29f2b1d4 --- /dev/null +++ b/sources/Service/Base/iDataPostProcessor.php @@ -0,0 +1,40 @@ +Rewind(); + + // Iterate throw objects... + while ($oObject = $oDbObjectSet->Fetch()) { + + // In case ot indirect link + if ($sTargetField !== null) { + $oObject = MetaModel::GetObject($sTargetClass, $oObject->Get($sTargetField)); + } + + if (!utils::ShowObsoleteData() && $oObject->IsObsolete()) { + continue; + } + + // Append object key + $aResult[] = $oObject->GetKey(); + } + + return $aResult; + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + return []; + } + } + + /** + * Encode. + * + * Convert array from view to arrays used by UI.php to apply link set modifications. + * + * @see cmdbAbstractObject::PrepareValueFromPostedForm + * + * @param array $aElements Link set elements + * @param string $sLinkClass Link class name + * @param string|null $sExtKeyToRemote External key to remote + * + * @return array{to_be_created: array, to_be_deleted: array, to_be_added: array, to_be_removed: array} + */ + static public function Encode(array $aElements, string $sLinkClass, string $sExtKeyToRemote = null): array + { + // Result arrays + $aToBeCreate = []; + $aToBeDelete = []; + $aToBeAdd = []; + $aToBeRemove = []; + + // Iterate throw data... + foreach ($aElements as $aData) { + + switch ($aData['operation']) { + + // OPERATION ADD + case 'add': + if ($sExtKeyToRemote === null) { + // Direct link attach + $aToBeAdd[] = $aData['data']['key']; + } else { + // Indirect link creation + $aToBeCreate[] = [ + 'class' => $sLinkClass, + 'data' => [ + $sExtKeyToRemote => $aData['data']['key'], + ], + ]; + } + break; + + // OPERATION REMOVE + case 'remove': + if ($sExtKeyToRemote === null) { + // Direct link detach + $aToBeRemove[] = $aData['data']['key']; + } else { + // Indirect link deletion + foreach ($aData['data']['link_keys'] as $sKey) { + $aToBeDelete[] = $sKey; + } + } + break; + } + + } + + return [ + 'to_be_created' => $aToBeCreate, + 'to_be_deleted' => $aToBeDelete, + 'to_be_added' => $aToBeAdd, + 'to_be_removed' => $aToBeRemove, + ]; + } + + /** + * Convert string representation of an orm linked set to object ormLinkSet. + * + * @param string $sValue + * @param \ormLinkSet $oOrmLinkSet + * + */ + static public function StringToOrmLinkSet(string $sValue, ormLinkSet $oOrmLinkSet) + { + try { + $aItems = explode(" ", $sValue); + foreach ($aItems as $sItem) { + if (!empty($sItem)) { + $oItem = MetaModel::GetObject($oOrmLinkSet->GetClass(), intval($sItem)); + if (!utils::ShowObsoleteData() && $oItem->IsObsolete()) { + continue; + } + $oOrmLinkSet->AddItem($oItem); + } + } + } + catch (Exception $e) { + ExceptionLog::LogException($e); + } + } +} \ No newline at end of file diff --git a/sources/Service/Links/LinkSetModel.php b/sources/Service/Links/LinkSetModel.php new file mode 100644 index 000000000..84f2140ae --- /dev/null +++ b/sources/Service/Links/LinkSetModel.php @@ -0,0 +1,75 @@ +GetLinkedClass(), $oAttDef->GetExtKeyToRemote()); + + return $oLinkingAttDef->GetTargetClass(); + } else { + return $oAttDef->GetLinkedClass(); + } + } + catch (Exception $e) { + return 'unknown'; + } + } + + /** + * GetLinkedClass. + * + * @param AttributeLinkedSet $oAttDef + * + * @return string + */ + static public function GetLinkedClass(AttributeLinkedSet $oAttDef): string + { + return $oAttDef->GetLinkedClass(); + } + + /** + * GetTargetField. + * + * @param AttributeLinkedSet $oAttDef + * + * @return string|null + */ + static public function GetTargetField(AttributeLinkedSet $oAttDef): ?string + { + if ($oAttDef instanceof AttributeLinkedSetIndirect) { + return $oAttDef->GetExtKeyToRemote(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/sources/Service/Links/LinkSetRepository.php b/sources/Service/Links/LinkSetRepository.php new file mode 100644 index 000000000..d13a541fa --- /dev/null +++ b/sources/Service/Links/LinkSetRepository.php @@ -0,0 +1,100 @@ +OptimizeColumnLoad([ + $sTargetClass => $aFieldsToLoad, + ]); + + // Prepare result + $aResult = []; + + // Iterate throw objects... + $oDbObjectSet->Rewind(); + while ($oObject = $oDbObjectSet->Fetch()) { + + // Ignore obsolete data + if (!utils::ShowObsoleteData() && $oObject->IsObsolete()) { + continue; + } + + // Prepare objet data + $aObjectData = []; + + // Link keys + $aObjectData['link_keys'] = [$oObject->GetKey()]; + + // In case ot indirect link + if ($sTargetField != null) { + $oObject = MetaModel::GetObject($sTargetClass, $oObject->Get($sTargetField)); + } + + // Remote key + $aObjectData['key'] = $oObject->GetKey(); + + // Fill loaded columns... + foreach ($aFieldsToLoad as $sField) { + $aObjectData[$sField] = $oObject->Get($sField); + } + + // Compute others data + $aResult[] = ObjectRepository::ComputeOthersData($oObject, $sTargetClass, $aObjectData, $aComplementAttributeSpec, $sObjectImageAttCode); + } + + return $aResult; + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + + return null; + } + } + +} \ No newline at end of file diff --git a/sources/Service/Links/LinksBulkDataPostProcessor.php b/sources/Service/Links/LinksBulkDataPostProcessor.php new file mode 100644 index 000000000..753cbaff3 --- /dev/null +++ b/sources/Service/Links/LinksBulkDataPostProcessor.php @@ -0,0 +1,139 @@ +GetColumnAsArray('id', false); + $sBulkList = implode(',', $aBulksObjects); + + // Get all links attached to object selection + $sOqlGroupBy = "SELECT $sLinkClass AS lnk WHERE lnk.$sOriginField IN ($sBulkList)"; + $oDbObjectSearch = DBSearch::FromOQL($sOqlGroupBy); + + // Group by links attached to object selection + $oFieldExp = new FieldExpression($sTargetField, 'lnk'); + $sQuery = $oDbObjectSearch->MakeGroupByQuery([$sTargetField], array('grouped_by_1' => $oFieldExp), true); + $aGroupResult = CMDBSource::QueryToArray($sQuery, MYSQLI_ASSOC); + + // Iterate throw result... + foreach ($aResult as &$aItem) { + + // Find group by object to extract link count + $aFound = null; + foreach ($aGroupResult as $aItemGroup) { + if ($aItem['key'] === $aItemGroup['grouped_by_1']) { + $aFound = $aItemGroup; + } + } + + // If found, get information + if ($aFound !== null) { + $aItem['group'] = 'Objects already linked'; + $aItem['occurrence'] = $aFound['_itop_count_']; + $aItem['occurrence_label'] = "Link on {$aFound['_itop_count_']} Objects(s)"; + $aItem['occurrence_info'] = "({$aFound['_itop_count_']})"; + $aItem['full'] = ($aFound['_itop_count_'] == $oDbObjectSetBulkObjects->Count()); + + // Retrieve linked objects keys + $sOqlLinkKeys = "SELECT $sLinkClass AS lnk WHERE lnk.$sOriginField IN ($sBulkList) AND lnk.$sTargetField = {$aItem['key']}"; + $oDbSearchLinkKeys = DBSearch::FromOQL($sOqlLinkKeys); + $aLinkedObjects = new DBObjectSet($oDbSearchLinkKeys); + $aItem['link_keys'] = $aLinkedObjects->GetColumnAsArray('id', false); + + } else { + $aItem['group'] = 'Others'; + $aItem['occurrence'] = ''; + $aItem['empty'] = true; + } + + } + + // Order items + usort($aResult, [self::class, "CompareItems"]); + } + catch (Exception $e) { + + ExceptionLog::LogException($e); + } + + } + + return $aResult; + } + + /** + * CompareItems. + * + * @param $aItemA + * @param $aItemB + * + * @return array|int + */ + static private function CompareItems($aItemA, $aItemB): int + { + if ($aItemA['occurrence'] === $aItemB['occurrence']) { + return 0; + } + + return ($aItemA['occurrence'] > $aItemB['occurrence']) ? -1 : 1; + } +} \ No newline at end of file diff --git a/templates/application/object/set/item_renderer.html.twig b/templates/application/object/set/item_renderer.html.twig new file mode 100644 index 000000000..77beebb4a --- /dev/null +++ b/templates/application/object/set/item_renderer.html.twig @@ -0,0 +1,15 @@ +
+ + {# Obsolescence #} + + + + + {# Friendly name #} + + + {# Additional content #} + {% block additional_content %} + {% endblock %} + +
\ No newline at end of file diff --git a/templates/application/object/set/option_renderer.html.twig b/templates/application/object/set/option_renderer.html.twig new file mode 100644 index 000000000..9eda721f8 --- /dev/null +++ b/templates/application/object/set/option_renderer.html.twig @@ -0,0 +1,29 @@ +
+ + {# Image #} + + + {# Desc #} + + + {# Obsolescnce #} + + + + + {# Friendly name #} + + + {# Additional content #} + {% block additional_content %} + {% endblock %} + + {# Additional field #} +
+ +
+ +
+ + +
\ No newline at end of file diff --git a/templates/application/object/set/set_renderer.html.twig b/templates/application/object/set/set_renderer.html.twig new file mode 100644 index 000000000..bf81b427c --- /dev/null +++ b/templates/application/object/set/set_renderer.html.twig @@ -0,0 +1,13 @@ +
+ + {# obsolescence #} + {% if obsolescence_flag %} + + + + {% endif %} + + {# friendly name #} + {{ friendlyname }} + +
\ No newline at end of file diff --git a/templates/base/components/input/layout.html.twig b/templates/base/components/input/layout.html.twig index e5238e30b..affbbe259 100644 --- a/templates/base/components/input/layout.html.twig +++ b/templates/base/components/input/layout.html.twig @@ -1,7 +1,8 @@ {% block iboInputLabel %} {% endblock %} {% block iboInput %} - + + +{# Options template #} +{% if oUIBlock.HasOptionsTemplate() %} + +{% endif %} + +{# Items template #} +{% if oUIBlock.HasItemsTemplate() %} + +{% endif %} + diff --git a/templates/base/components/input/set/layout.ready.js.twig b/templates/base/components/input/set/layout.ready.js.twig new file mode 100644 index 000000000..6bce425fd --- /dev/null +++ b/templates/base/components/input/set/layout.ready.js.twig @@ -0,0 +1,206 @@ + +{# @copyright Copyright (C) 2010-2021 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +{# SET WIDGET #} +{% set oDataProvider = oUIBlock.GetDataProvider() %} +let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({ + + {# Global options #} + {% if oDataProvider.IsAjaxProviderType %} + preload: true, {# call ajax directly #} + loadingClass: '', + {% endif %} + itemClass: 'item attribute-set-item', + hasError: {{ oUIBlock.HasError()|var_export() }}, + placeholder: '{{ oUIBlock.GetPlaceholder() }}', + + {# Remove button plugin #} + plugins: { + {# PLUGIN update operations #} + 'combodo_update_operations' : {}, + {# PLUGIN combodo auto position #} + 'combodo_auto_position' : { + maxDropDownHeight: 300, {# in px #} + }, + {# PLUGIN combodo add button #} + {% if oUIBlock.HasAddOptionButton() %} + 'combodo_add_button' : { + title: '{{ oUIBlock.GetAddButtonTitle() }}' + }, + {% endif %} + {% if oUIBlock.IsMultiValuesSynthesis() %} + 'combodo_multi_values_synthesis' : { + + tooltip_links_will_be_created_for_all_objects: '{{ 'UI:Links:Bulk:LinkWillBeCreatedForAllObjects'|dict_s }}', + tooltip_links_will_be_deleted_from_all_objects: '{{ 'UI:Links:Bulk:LinkWillBeDeletedFromAllObjects'|dict_s }}', + tooltip_links_will_be_created_for_one_object: '{{ 'UI:Links:Bulk:LinkWillBeCreatedFor1Object'|dict_s }}', + tooltip_links_will_be_deleted_from_one_object: '{{ 'UI:Links:Bulk:LinkWillBeDeletedFrom1Object'|dict_s }}', + tooltip_links_will_be_created_for_x_objects: '{{ 'UI:Links:Bulk:LinkWillBeCreatedForXObjects'|dict_s }}', + tooltip_links_will_be_deleted_from_x_objects: '{{ 'UI:Links:Bulk:LinkWillBeDeletedFromXObjects'|dict_s }}', + tooltip_links_exist_for_all_objects: '{{ 'UI:Links:Bulk:LinkExistForAllObjects'|dict_s }}', + tooltip_links_exist_for_one_object: '{{ 'UI:Links:Bulk:LinkExistForOneObject'|dict_s }}', + tooltip_links_exist_for_x_objects: '{{ 'UI:Links:Bulk:LinkExistForXObjects'|dict_s }}', + }, + {% endif %} + {# PLUGIN remove button #} + {% if oUIBlock.HasRemoveItemButton() %} + 'remove_button' : {}, + {% endif %} + }, + + {# Max items you can select #} + {% if oUIBlock.GetMaxItems() is not empty %} + maxItems: {{ oUIBlock.GetMaxItems() }}, + {% endif %} + + {# Max options available #} + {% if oUIBlock.GetMaxOptions() is not empty %} + maxOptions: {{ oUIBlock.GetMaxOptions() }}, + {% endif %} + + {# Data fields #} + valueField: '{{ oDataProvider.GetDataValueField() }}', + labelField: '{{ oDataProvider.GetDataLabelField() }}', + searchField: {{ oDataProvider.GetDataSearchFields()|json_encode()|raw }}, + optgroupField: '{{ oDataProvider.GetGroupField() }}', + tooltipField: '{{ oDataProvider.GetTooltipField() }}', + + {# Initial options data, may be oveeride by ajax load method #} + options: {{ oDataProvider.GetOptions()|json_encode()|raw }}, + + {# Groups data #} + optgroups: {{ oDataProvider.GetOptionsGroups()|json_encode()|raw }}, + + {# Items data #} + initial: {{ oUIBlock.GetValue()|raw }}, + items: {{ oUIBlock.GetValue()|raw }}, + + inputClass: 'ibo-input ibo-input-selectize ibo-input-set attribute-set selectize-input', + + {# Ajax data load #} + {% if oDataProvider.IsAjaxProviderType %} + load: function (query, callback) { + let me = this; + $.ajax({ + url: '{{ get_absolute_url_app_root() }}pages/ajax.render.php?route={{ oDataProvider.GetRoute() }}&search=' + query + '{{ oDataProvider.GetParamsAsQueryString|raw }}', + type: 'POST', + dataType: 'json', + data: me.convertParamArray('{{ oDataProvider.GetPostParamsAsJsonString()|raw }}'), + error: function (e) { + callback(); + console.error(e); + if(!me.settings.hasError) { + me.toggleErrorClass(true); + } + }, + success: function (res) { + + // Handle errors + if(!me.settings.hasError){ + me.toggleErrorClass(!res.data.success); + if(!res.data.success) return; + } + + // Retrieve current input value + let aSelectedItems = me.getValue(); + // Filter old options data to keep selected values + let options = Object.values(me.options); + options = options.filter(item => aSelectedItems.includes(item['{{ oDataProvider.GetDataValueField() }}'])); + // Merge kept and new values + options = $.merge(options, res.data.search_data); + // Compute groups + $.each(options, function(index, value) { + me.addOptionGroup(value['{{ oDataProvider.GetGroupField() }}'], { + label: value['{{ oDataProvider.GetGroupField() }}'], + value: value['{{ oDataProvider.GetGroupField() }}'] + }); + }); + // Clear all options + me.clearOptions(); + // Add merged values + callback(options); + // Restore input value + me.addItems(aSelectedItems, true); + } + }); + }, + {% endif %} + + {# Renderers #} + render: { + + {# Options #} + {% if oUIBlock.HasOptionsTemplate() %} + option: function(option) { + return CombodoGlobalToolbox.RenderTemplate('#{{ oUIBlock.GetId() }}_options_template', option, this.settings.optionClass)[0].outerHTML; + }, + {% endif %} + + {# Items #} + {% if oUIBlock.HasItemsTemplate() %} + item: function (item) { + return CombodoGlobalToolbox.RenderTemplate('#{{ oUIBlock.GetId() }}_items_template', item, this.settings.itemClass)[0].outerHTML; + }, + {% endif %} + + }, + + onInitialize: function(){ + + /** + * Function to convert param array. + * + * EVAL_JAVASCRIPT{code_to_eval} + */ + this.convertParamArray = function(paramArray){ + let postParam = JSON.parse(paramArray); + let data = {}; + Object.entries(postParam).forEach(([key, value]) => { + let matches = null; + if(typeof(value) === 'string'){ + matches = value.match(/^EVAL_JAVASCRIPT{(.*)}$/); + } + if(matches != null){ + data[key] = eval(matches[1]); + } + else{ + data[key] = value; + } + }); + return data; + }; + + /** + * Function to show error. + * + */ + this.toggleErrorClass = function(bValue){ + this.$control.toggleClass('selectize-input-error', bValue); + }; + + {# Error #} + this.toggleErrorClass(this.settings.hasError); + }, + + + {# On item add #} + onItemAdd: function(value, $item){ + $item.addClass(this.settings.itemClass); + $item.attr({ + 'data-tooltip-content': this.options[value][this.settings.tooltipField], + 'data-tooltip-html-enabled': true + }); + CombodoTooltip.InitTooltipFromMarkup($item); + }, + + {# plugin combodo_add_button #} + {% if oUIBlock.HasAddOptionButton() %} + onAdd: function(){ + alert('todo on add option'); + }, + {% endif %} +}); + + + diff --git a/templates/base/components/input/set/simple_option_renderer.html.twig b/templates/base/components/input/set/simple_option_renderer.html.twig new file mode 100644 index 000000000..e4d44e32e --- /dev/null +++ b/templates/base/components/input/set/simple_option_renderer.html.twig @@ -0,0 +1,15 @@ +{# @copyright Copyright (C) 2010-2021 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + +
+ +
+ + + + + + +
+ +
\ No newline at end of file diff --git a/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php b/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php index ea7ed9bb5..ec5ad0613 100644 --- a/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php +++ b/tests/manual-visual-tests/Backoffice/RenderAllUiBlocks.php @@ -28,13 +28,12 @@ use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroup; use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroupUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSection; -use Combodo\iTop\Application\UI\Base\Component\Dashlet\DashletBadge; -use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTable; use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Field\FieldUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSet; use Combodo\iTop\Application\UI\Base\Component\Html\Html; use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory; +use Combodo\iTop\Application\UI\Base\Component\Input\Set\SetUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Panel\Panel; use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Pill\PillFactory; @@ -407,24 +406,163 @@ $oPage->AddUiBlock(DataTableUIBlockFactory::MakeForStaticData('Static datatable' ), array( 'a' => 'A3', 'b' => 'B3', 'c' => 'C3', 'd' => 'D3' ), array( - 'a' => 'A4', 'b' => 'B4', 'c' => 'C4', 'd' => 'D4' - ),array( - '@class' => 'ibo-is-red','a' => 'A5 (Red highlighting)', 'b' => 'B5', 'c' => 'C5', 'd' => 'D5' - ),array( - '@class' => 'ibo-is-danger','a' => 'A6 (Danger highlighting)', 'b' => 'B6', 'c' => 'C6', 'd' => 'D6' - ),array( - '@class' => 'ibo-is-orange','a' => 'A7 (Orange highlighting)', 'b' => 'B7', 'c' => 'C7', 'd' => 'D7' - ),array( - '@class' => 'ibo-is-warning','a' => 'A8 (Warning highlighting)', 'b' => 'B8', 'c' => 'C8', 'd' => 'D8' - ),array( - '@class' => 'ibo-is-blue','a' => 'A9 (Blue highlighting)', 'b' => 'B9', 'c' => 'C9', 'd' => 'D9' - ),array( - '@class' => 'ibo-is-info','a' => 'A10 (Info highlighting)', 'b' => 'B10', 'c' => 'C10', 'd' => 'D10' - ),array( - '@class' => 'ibo-is-green','a' => 'A11 (Green highlighting)', 'b' => 'B11', 'c' => 'C11', 'd' => 'D11' - ),array( - '@class' => 'ibo-is-success','a' => 'A12 (Success highlighting)', 'b' => 'B12', 'c' => 'C12', 'd' => 'D12' - ), -))); + 'a' => 'A4', + 'b' => 'B4', + 'c' => 'C4', + 'd' => 'D4', + ), + array( + '@class' => 'ibo-is-red', + 'a' => 'A5 (Red highlighting)', + 'b' => 'B5', + 'c' => 'C5', + 'd' => 'D5', + ), + array( + '@class' => 'ibo-is-danger', + 'a' => 'A6 (Danger highlighting)', + 'b' => 'B6', + 'c' => 'C6', + 'd' => 'D6', + ), + array( + '@class' => 'ibo-is-orange', + 'a' => 'A7 (Orange highlighting)', + 'b' => 'B7', + 'c' => 'C7', + 'd' => 'D7', + ), + array( + '@class' => 'ibo-is-warning', + 'a' => 'A8 (Warning highlighting)', + 'b' => 'B8', + 'c' => 'C8', + 'd' => 'D8', + ), + array( + '@class' => 'ibo-is-blue', + 'a' => 'A9 (Blue highlighting)', + 'b' => 'B9', + 'c' => 'C9', + 'd' => 'D9', + ), + array( + '@class' => 'ibo-is-info', + 'a' => 'A10 (Info highlighting)', + 'b' => 'B10', + 'c' => 'C10', + 'd' => 'D10', + ), + array( + '@class' => 'ibo-is-green', + 'a' => 'A11 (Green highlighting)', + 'b' => 'B11', + 'c' => 'C11', + 'd' => 'D11', + ), + array( + '@class' => 'ibo-is-success', + 'a' => 'A12 (Success highlighting)', + 'b' => 'B12', + 'c' => 'C12', + 'd' => 'D12', + ), + ))); + +///////// +// Set +///////// + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Set examples', 2)); + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Simple', 4)); + +$aOptions = [ + [ + 'label' => 'Chien', + 'value' => 'dog', + 'icon' => 'fas fa-dog', + 'group' => 'Domestique', + ], + [ + 'label' => 'Chat', + 'value' => 'cat', + 'icon' => 'fas fa-cat', + 'group' => 'Domestique', + ], + [ + 'label' => 'Cheval', + 'value' => 'horse', + 'icon' => 'fas fa-horse', + 'group' => 'Domestique', + ], + [ + 'label' => 'Araignée', + 'value' => 'spider', + 'icon' => 'fas fa-spider', + 'class' => 'demo_set', + 'group' => 'Sauvage', + ], + [ + 'label' => 'Otarie', + 'value' => 'otter', + 'icon' => 'fas fa-otter', + 'group' => 'Sauvage', + ], + [ + 'label' => 'Poisson', + 'value' => 'fish', + 'icon' => 'fas fa-fish', + 'group' => 'Domestique', + ], + [ + 'label' => 'Grenouille', + 'value' => 'frog', + 'icon' => 'fas fa-frog', + 'group' => 'Sauvage', + ], + [ + 'label' => 'Hippopotame', + 'value' => 'hippo', + 'icon' => 'fas fa-hippo', + 'group' => 'Sauvage', + ], +]; +$oPage->add_style('.demo_set{color:red;}'); + +$oSimpleSetBlock = SetUIBlockFactory::MakeForSimple('SetSimple', $aOptions, 'label', 'value', ['label']); +$oSimpleSetBlock->SetName('SimpleSetBlock'); +$oPage->AddUiBlock($oSimpleSetBlock); + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Add Option Button', 3)); + +$oSimpleAddSetBlock = SetUIBlockFactory::MakeForSimple('SetWithAddOption', $aOptions, 'label', 'value', ['label']); +$oSimpleAddSetBlock->SetName('SetWithAddOption'); +$oSimpleAddSetBlock->SetHasAddOptionButton(true); +$oPage->AddUiBlock($oSimpleAddSetBlock); + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Renderer', 3)); + +$oSimpleSetBlockRenderer = SetUIBlockFactory::MakeForSimple('SetRenderer', $aOptions, 'label', 'value', ['label']); +$oSimpleSetBlockRenderer->SetName('SimpleSetBlockWithRenderer'); +$oSimpleSetBlockRenderer->SetOptionsTemplate('base/components/input/set/simple_option_renderer.html.twig'); +$oSimpleSetBlockRenderer->SetItemsTemplate('base/components/input/set/simple_option_renderer.html.twig'); +$oPage->AddUiBlock($oSimpleSetBlockRenderer); + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('Grouping', 3)); + +$oSimpleSetBlockGroup = SetUIBlockFactory::MakeForSimple('SetGroup', $aOptions, 'label', 'value', ['label'], 'group'); +$oSimpleSetBlockGroup->SetName('SimpleSetBlockWithGroup'); +$oPage->AddUiBlock($oSimpleSetBlockGroup); + +$oPage->AddUiBlock(TitleUIBlockFactory::MakeNeutral('OQL', 3)); + +$oSimpleSetBlockOql = SetUIBlockFactory::MakeForOQL('SetOql', 'Person', 'SELECT Person'); +$oSimpleSetBlockOql->SetName('OqlSet'); +$oPage->AddUiBlock($oSimpleSetBlockOql); + +$oSimpleSetBlockOql2 = SetUIBlockFactory::MakeForOQL('SetOql2', 'Location', 'SELECT Location'); +$oSimpleSetBlockOql2->SetName('OqlSet2'); +$oPage->AddUiBlock($oSimpleSetBlockOql2); $oPage->output();