diff --git a/datamodels/2.x/itop-attachments/main.itop-attachments.php b/datamodels/2.x/itop-attachments/main.itop-attachments.php index fe3e7b3ff..1fbe5a8b0 100644 --- a/datamodels/2.x/itop-attachments/main.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/main.itop-attachments.php @@ -64,6 +64,25 @@ class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExt return $result; } + /** + * @param cmdbAbstractObject $oObject + * + * @return bool + * @since 3.2.1 N°7534 + */ + public static function IsAttachmentAllowedForObject(cmdbAbstractObject $oObject) : bool + { + $aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket')); + foreach ($aAllowedClasses as $sAllowedClass) + { + if ($oObject instanceof $sAllowedClass) + { + return true; + } + } + return false; + } + /** * Returns the max. file upload size allowed as a dictionary entry * diff --git a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml index eff609aa6..a86d8e125 100644 --- a/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml +++ b/datamodels/2.x/itop-portal-base/portal/config/routes/object_brick.yaml @@ -97,6 +97,12 @@ p_object_attachment_add: defaults: _controller: 'Combodo\iTop\Portal\Controller\ObjectController::AttachmentAction' +p_object_attachment_display: + path: '/object/attachment/display/{sAttachmentId}' + defaults: + _controller: 'Combodo\iTop\Portal\Controller\ObjectController::AttachmentAction' + sOperation: 'display' + p_object_attachment_download: path: '/object/attachment/download/{sAttachmentId}' defaults: diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index 9dd64cfbb..a1db8de6f 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -1241,6 +1241,19 @@ class ObjectController extends BrickController break; + case 'display': + // Preparing redirection + // - Route + $aRouteParams = array( + 'sObjectClass' => 'Attachment', + 'sObjectId' => $this->oRequestManipulatorHelper->ReadParam('sAttachmentId', null), + 'sObjectField' => 'contents', + ); + + $oResponse = $this->ForwardToRoute('p_object_document_display', $aRouteParams, $oRequest->query->all()); + + break; + default: throw new HttpException(Response::HTTP_FORBIDDEN, Dict::S('Error:HTTP:400')); break; diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php index 5840f2618..21ae362ca 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php @@ -94,6 +94,8 @@ class ObjectFormManager extends FormManager */ private $oFormHandlerHelper; + /** @var array $aPlugins plugins data */ + private array $aPlugins = array(); /** * @param string|array $formManagerData value of the formmanager_data portal parameter, either JSON or object @@ -502,6 +504,11 @@ class ObjectFormManager extends FormManager { $aFieldsExtraData[$sFieldId]['opened'] = true; } + // Checking if the field is handled by a plugin + if ($oFieldNode->hasAttribute('data-field-plugin')) + { + $aFieldsExtraData[$sFieldId]['plugin'] = $oFieldNode->getAttribute('data-field-plugin'); + } // Checking if field allows to ignore scope (For linked set) if ($oFieldNode->hasAttribute('data-field-ignore-scopes') && ($oFieldNode->getAttribute('data-field-ignore-scopes') === 'true')) { @@ -651,6 +658,19 @@ class ObjectFormManager extends FormManager // Building the form foreach ($aFieldsAtts as $sAttCode => $iFieldFlags) { + // handle plugins fields + if(array_key_exists('plugin', $aFieldsExtraData[$sAttCode])){ + $sPluginName = $aFieldsExtraData[$sAttCode]['plugin']; + switch($sPluginName){ + case AttachmentPlugIn::class: + $this->AddAttachmentField($oForm, $sAttCode, $aFieldsExtraData); + break; + default: + throw new Exception('Unknown plugin ' . $sPluginName); + } + continue; + } + $oAttDef = MetaModel::GetAttributeDef(get_class($this->oObject), $sAttCode); /** @var Field $oField */ @@ -990,49 +1010,12 @@ class ObjectFormManager extends FormManager } } - // Checking if the instance has attachments - if (class_exists('Attachment') && class_exists('AttachmentPlugIn')) - { - // Checking if the object is allowed for attachments - $bClassAllowed = false; - $aAllowedClasses = MetaModel::GetModuleSetting('itop-attachments', 'allowed_classes', array('Ticket')); - foreach ($aAllowedClasses as $sAllowedClass) - { - if ($this->oObject instanceof $sAllowedClass) - { - $bClassAllowed = true; - break; - } - } - - // Adding attachment field - if ($bClassAllowed) - { - // set id to a unique key - avoid collisions with another attribute that could exist with the name 'attachments' - $oField = new FileUploadField('attachments_plugin'); - $oField->SetLabel(Dict::S('Portal:Attachments')) - ->SetUploadEndpoint($this->oFormHandlerHelper->GetUrlGenerator()->generate('p_object_attachment_add')) - ->SetDownloadEndpoint($this->oFormHandlerHelper->GetUrlGenerator()->generate('p_object_attachment_download', - array('sAttachmentId' => '-sAttachmentId-'))) - ->SetTransactionId($oForm->GetTransactionId()) - ->SetAllowDelete($this->oFormHandlerHelper->getCombodoPortalConf()['properties']['attachments']['allow_delete']) - ->SetObject($this->oObject); - - // Checking if we can edit attachments in the current state - if (($this->sMode === static::ENUM_MODE_VIEW) - || AttachmentPlugIn::IsReadonlyState($this->oObject, $this->oObject->GetState(), - AttachmentPlugIn::ENUM_GUI_PORTALS) === true - || $oForm->GetEditableFieldCount(true) === 0) - { - $oField->SetReadOnly(true); - } - - // Adding attachements field in transition only if it is editable - if (!$this->IsTransitionForm() || ($this->IsTransitionForm() && !$oField->GetReadOnly())) - { - $oForm->AddField($oField); - } - } + // fallback Checking if the instance has attachments + // (in case attachment is not explicitly declared in layout) + if (class_exists('Attachment') && class_exists('AttachmentPlugIn') + && !$this->IsPluginInitialized(AttachmentPlugIn::class) + && AttachmentPlugIn::IsAttachmentAllowedForObject($this->oObject)){ + $this->AddAttachmentField($oForm, 'attachments_plugin', $aFieldsExtraData); } $oForm->Finalize(); @@ -1040,6 +1023,78 @@ class ObjectFormManager extends FormManager $this->oRenderer->SetForm($this->oForm); } + /** + * IsPluginInitialized. + * + * @param string $sPluginName + * + * @return bool + */ + private function IsPluginInitialized(string $sPluginName) : bool + { + return array_key_exists($sPluginName, $this->aPlugins); + } + + /** + * AddAttachmentField. + * + * @param $oForm + * @param $sId + * @param $aFieldsExtraData + * + * @throws \Exception + */ + private function AddAttachmentField($oForm, $sId, $aFieldsExtraData) : void + { + // only one instance allowed + if($this->IsPluginInitialized(AttachmentPlugIn::class)){ + throw new Exception("Unable to process field `$sId`, AttachmentPlugIn has already been initialized with field `" . $this->aPlugins[AttachmentPlugIn::class]['field']->GetId() . '`'); + } + + // not allowed for object class + if(!AttachmentPlugIn::IsAttachmentAllowedForObject($this->oObject)){ + throw new Exception("Unable to process field `$sId`, AttachmentPlugIn is not allowed for class `" . $this->oObject::class . '`'); + } + + // set id to a unique key - avoid collisions with another attribute that could exist with the name 'attachments' + $oField = new FileUploadField($sId); + $oField->SetLabel(Dict::S('Portal:Attachments')) + ->SetUploadEndpoint($this->oFormHandlerHelper->GetUrlGenerator()->generate('p_object_attachment_add')) + ->SetDownloadEndpoint($this->oFormHandlerHelper->GetUrlGenerator()->generate('p_object_attachment_download', + array('sAttachmentId' => '-sAttachmentId-'))) + ->SetDisplayEndpoint($this->oFormHandlerHelper->GetUrlGenerator()->generate('p_object_attachment_display', + array('sAttachmentId' => '-sAttachmentId-'))) + ->SetTransactionId($oForm->GetTransactionId()) + ->SetAllowDelete($this->oFormHandlerHelper->getCombodoPortalConf()['properties']['attachments']['allow_delete']) + ->SetObject($this->oObject); + + // Checking if we can edit attachments in the current state + $oObjectFormManager = $this; + $oField->SetOnFinalizeCallback(function() use ($oObjectFormManager, $oForm, $oField){ + if (($oObjectFormManager->sMode === static::ENUM_MODE_VIEW) + || AttachmentPlugIn::IsReadonlyState($oObjectFormManager->oObject, $oObjectFormManager->oObject->GetState(), + AttachmentPlugIn::ENUM_GUI_PORTALS) === true + || $oForm->GetEditableFieldCount(true) === 0) + { + $oField->SetReadOnly(true); + } + }); + + if (array_key_exists($sId, $aFieldsExtraData) && array_key_exists('opened', $aFieldsExtraData[$sId])){ + $oField->SetDisplayOpened(true); + } + + // Adding attachments field in transition only if it is editable + if (!$this->IsTransitionForm() || !$oField->GetReadOnly()){ + $oForm->AddField($oField); + } + + // save plugin data + $this->aPlugins[AttachmentPlugIn::class] = [ + 'field' => $oField, + ]; + } + /** * @inheritDoc * diff --git a/sources/Form/Field/FileUploadField.php b/sources/Form/Field/FileUploadField.php index 41944c78c..ab08c4ea3 100644 --- a/sources/Form/Field/FileUploadField.php +++ b/sources/Form/Field/FileUploadField.php @@ -30,6 +30,11 @@ class FileUploadField extends AbstractSimpleField { /** @var bool DEFAULT_ALLOW_DELETE */ const DEFAULT_ALLOW_DELETE = true; + /** + * @var bool DEFAULT_DISPLAY_OPENED + * @since 3.2.1 N°7534 + */ + const DEFAULT_DISPLAY_OPENED = false; /** @var string|null $sTransactionId */ protected $sTransactionId; @@ -39,8 +44,18 @@ class FileUploadField extends AbstractSimpleField protected $sUploadEndpoint; /** @var string|null $sDownloadEndpoint */ protected $sDownloadEndpoint; + /** + * @var string|null $sViewEndpoint + * @since 3.2.1 N°7534 + */ + protected ?string $sDisplayEndpoint; /** @var bool $bAllowDelete */ protected $bAllowDelete; + /** + * @var bool $bDisplayOpened + * @since 3.2.1 N°7534 + */ + protected bool $bDisplayOpened; /** * @inheritDoc @@ -51,7 +66,9 @@ class FileUploadField extends AbstractSimpleField $this->oObject = null; $this->sUploadEndpoint = null; $this->sDownloadEndpoint = null; + $this->sDisplayEndpoint = null; $this->bAllowDelete = static::DEFAULT_ALLOW_DELETE; + $this->bDisplayOpened = static::DEFAULT_DISPLAY_OPENED; parent::__construct($sId, $onFinalizeCallback); } @@ -134,6 +151,27 @@ class FileUploadField extends AbstractSimpleField return $this; } + /** + * @return string|null + * @since 3.2.1 N°7534 + */ + public function GetDisplayEndpoint(): ?string + { + return $this->sDisplayEndpoint; + } + + /** + * @param string $sDisplayEndpoint + * + * @return FileUploadField + * @since 3.2.1 N°7534 + */ + public function SetDisplayEndpoint(string $sDisplayEndpoint): FileUploadField + { + $this->sDisplayEndpoint = $sDisplayEndpoint; + return $this; + } + /** * @return bool */ @@ -153,4 +191,29 @@ class FileUploadField extends AbstractSimpleField return $this; } + /** + * Sets if the field should be displayed opened on initialization + * + * @param bool $bDisplayOpened + * + * @return FileUploadField + * @since 3.2.1 N°7534 + */ + public function SetDisplayOpened(bool $bDisplayOpened) : FileUploadField + { + $this->bDisplayOpened = $bDisplayOpened; + + return $this; + } + + /** + * Returns if the field should be displayed opened on initialization + * + * @return boolean + * @since 3.2.1 N°7534 + */ + public function GetDisplayOpened() : bool + { + return $this->bDisplayOpened; + } } diff --git a/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php b/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php index 7dc676fc3..c3332cecf 100644 --- a/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php +++ b/sources/Renderer/Bootstrap/FieldRenderer/BsFileUploadFieldRenderer.php @@ -80,11 +80,17 @@ class BsFileUploadFieldRenderer extends BsFieldRenderer $sFieldWrapperId = 'form_upload_wrapper_' . $this->oField->GetGlobalId(); $sFieldDescriptionForHTMLTag = ($this->oField->HasDescription()) ? 'data-tooltip-content="'.utils::HtmlEntities($this->oField->GetDescription()).'"' : ''; - // If collapsed - $sCollapseTogglerClass .= ' collapsed'; - $sCollapseTogglerExpanded = 'false'; - $sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass; - $sCollapseJSInitState = 'false'; + // Preparing collapsed state + if ($this->oField->GetDisplayOpened()) { + $sCollapseTogglerExpanded = 'true'; + $sCollapseTogglerIconClass = $sCollapseTogglerIconVisibleClass; + $sCollapseJSInitState = 'true'; + } else { + $sCollapseTogglerClass .= ' collapsed'; + $sCollapseTogglerExpanded = 'false'; + $sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass; + $sCollapseJSInitState = 'false'; + } // Label $oOutput->AddHtml('