mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 02:28:44 +02:00
N°7534 - Request to Improve the Display of an Inline Attachment (#664)
This commit is contained in:
@@ -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
|
||||
*
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('<div class="form_field_label">');
|
||||
@@ -183,6 +189,7 @@ JS
|
||||
'{{iAttId}}',
|
||||
'{{sLineStyle}}',
|
||||
'{{sDocDownloadUrl}}',
|
||||
'{{sDocDisplayUrl}}',
|
||||
true,
|
||||
'{{sAttachmentThumbUrl}}',
|
||||
'{{sFileName}}',
|
||||
@@ -234,6 +241,7 @@ JS
|
||||
var \$oAttachmentTBody = $(this).closest('.fileupload_field_content').find('.attachments_container table#$sAttachmentTableId>tbody'),
|
||||
iAttId = data.result.att_id,
|
||||
sDownloadLink = '{$this->oField->GetDownloadEndpoint()}'.replace(/-sAttachmentId-/, iAttId),
|
||||
sDisplayLink = '{$this->oField->GetDisplayEndpoint()}'.replace(/-sAttachmentId-/, iAttId),
|
||||
sAttachmentMeta = '<input id="attachment_'+iAttId+'" type="hidden" name="attachments[]" value="'+iAttId+'"/>';
|
||||
|
||||
// hide "no attachment" line if present
|
||||
@@ -247,6 +255,7 @@ JS
|
||||
{search: "{{iAttId}}", replace:iAttId },
|
||||
{search: "{{lineStyle}}", replace:'' },
|
||||
{search: "{{sDocDownloadUrl}}", replace:sDownloadLink },
|
||||
{search: "{{sDocDisplayUrl}}", replace:sDisplayLink },
|
||||
{search: "{{sAttachmentThumbUrl}}", replace:data.result.icon },
|
||||
{search: "{{sFileName}}", replace: data.result.msg },
|
||||
{search: "{{sAttachmentMeta}}", replace:sAttachmentMeta },
|
||||
@@ -402,6 +411,7 @@ HTML
|
||||
$sFileName = utils::EscapeHtml($oDoc->GetFileName());
|
||||
|
||||
$sDocDownloadUrl = str_replace('-sAttachmentId-', $iAttId, $this->oField->GetDownloadEndpoint());
|
||||
$sDocDisplayUrl = str_replace('-sAttachmentId-', $iAttId, $this->oField->GetDisplayEndpoint());
|
||||
|
||||
$sAttachmentThumbUrl = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName);
|
||||
$bHasPreview = false;
|
||||
@@ -431,6 +441,7 @@ HTML
|
||||
$iAttId,
|
||||
$sLineStyle,
|
||||
$sDocDownloadUrl,
|
||||
$sDocDisplayUrl,
|
||||
$bHasPreview,
|
||||
$sAttachmentThumbUrl,
|
||||
$sFileName,
|
||||
@@ -485,6 +496,7 @@ HTML;
|
||||
* @param int $iAttId
|
||||
* @param string $sLineStyle
|
||||
* @param string $sDocDownloadUrl
|
||||
* @param string $sDocDisplayUrl
|
||||
* @param bool $bHasPreview replace string $sIconClass since 3.0.1
|
||||
* @param string $sAttachmentThumbUrl
|
||||
* @param string $sFileName
|
||||
@@ -499,7 +511,7 @@ HTML;
|
||||
* @since 2.7.0
|
||||
*/
|
||||
protected static function GetAttachmentTableRow(
|
||||
$iAttId, $sLineStyle, $sDocDownloadUrl, $bHasPreview, $sAttachmentThumbUrl, $sFileName, $sAttachmentMeta, $sFileSize,
|
||||
$iAttId, $sLineStyle, $sDocDownloadUrl, $sDocDisplayUrl, $bHasPreview, $sAttachmentThumbUrl, $sFileName, $sAttachmentMeta, $sFileSize,
|
||||
$iFileSizeRaw, $iFileDownloadsCount, $sAttachmentDate, $iAttachmentDateRaw, $bIsDeleteAllowed
|
||||
) {
|
||||
$sDeleteCell = '';
|
||||
@@ -511,16 +523,16 @@ HTML;
|
||||
$sHtml = "<tr id=\"display_attachment_{$iAttId}\" class=\"attachment\" $sLineStyle>";
|
||||
|
||||
if($bHasPreview) {
|
||||
$sHtml .= "<td role=\"icon\"><a href=\"$sDocDownloadUrl\" target=\"_blank\" data-tooltip-content=\"<img class='attachment-tooltip' src='{$sDocDownloadUrl}'>\" data-tooltip-html-enabled=true><img src=\"$sAttachmentThumbUrl\" ></a></td>";
|
||||
$sHtml .= "<td role=\"icon\"><a href=\"$sDocDisplayUrl\" target=\"_blank\" data-tooltip-content=\"<img class='attachment-tooltip' src='{$sDocDownloadUrl}'>\" data-tooltip-html-enabled=true><img src=\"$sAttachmentThumbUrl\" ></a></td>";
|
||||
} else {
|
||||
$sHtml .= "<td role=\"icon\"><a href=\"$sDocDownloadUrl\" target=\"_blank\"><img src=\"$sAttachmentThumbUrl\" ></a></td>";
|
||||
$sHtml .= "<td role=\"icon\"><a href=\"$sDocDisplayUrl\" target=\"_blank\"><img src=\"$sAttachmentThumbUrl\" ></a></td>";
|
||||
}
|
||||
|
||||
$sHtml .= <<<HTML
|
||||
<td role="filename"><a href="$sDocDownloadUrl" target="_blank">$sFileName</a>$sAttachmentMeta</td>
|
||||
<td role="filename"><a href="$sDocDisplayUrl" target="_blank">$sFileName</a>$sAttachmentMeta</td>
|
||||
<td role="formatted-size" data-order="$iFileSizeRaw">$sFileSize</td>
|
||||
<td role="upload-date" data-order="$iAttachmentDateRaw">$sAttachmentDate</td>
|
||||
<td role="downloads-count">$iFileDownloadsCount</td>
|
||||
<td role="downloads-count"><a href="$sDocDownloadUrl" target="_blank"><span class="fas fa-download fa-lg" style="float: right;"></span></a>$iFileDownloadsCount</td>
|
||||
$sDeleteCell
|
||||
</tr>
|
||||
HTML;
|
||||
|
||||
@@ -254,7 +254,6 @@ JS
|
||||
CombodoCKEditorHandler.GetInstance("#{$this->oField->GetGlobalId()}")
|
||||
.then((oCKEditor) => {
|
||||
oCKEditor.model.document.on("change:data", () => {
|
||||
console.log("desc changed!");
|
||||
const oFieldElem = $("#{$this->oField->GetGlobalId()}");
|
||||
oFieldElem.closest(".field_set").trigger("field_change", {
|
||||
id: oFieldElem.attr("id"),
|
||||
|
||||
Reference in New Issue
Block a user