diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 724a6b73c..d94843364 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -2069,7 +2069,7 @@ class AttributeLinkedSet extends AttributeDefinition public function GetImportColumns() { $aColumns = array(); - $aColumns[$this->GetCode()] = 'TEXT'.CMDBSource::GetSqlStringColumnDefinition(); + $aColumns[$this->GetCode()] = 'MEDIUMTEXT'.CMDBSource::GetSqlStringColumnDefinition(); return $aColumns; } diff --git a/css/backoffice/README.md b/css/backoffice/README.md index 52f550b1f..ec7e21051 100644 --- a/css/backoffice/README.md +++ b/css/backoffice/README.md @@ -1,75 +1,108 @@ -## Description -This is a brief description of the SASS 7-1 system and how to use it. -- [File structure](#file-structure) -- [Usage](#usage) +# Description +This is a brief description of the how the backoffice theme is structured using both BEM and SASS 7-1 systems and how to use them. + * [7-1 pattern](#7-1-pattern) + * [File structure](#file-structure) + * [Usage](#usage) + * [BEM methodology](#bem-methodology) + * [Principles](#principles) + * [Examples](#examples) +# 7-1 pattern ## File structure SCSS files are structured following the [7-1 pattern](https://sass-guidelin.es/#the-7-1-pattern). \ @rveitch made a great summary with the following, which can also be found [here](https://gist.github.com/rveitch/84cea9650092119527bc). -_Note: Folders with an * are customizations we made to the original 7-1 pattern to best fit our needs_ +_Note: Folders with an * are customizations we made to the original 7-1 pattern to best fit our needs_ ``` css/backoffice/ | |– utils/ -| |– _variables.scss # Sass Variables -| |– _functions.scss # Sass Functions -| |– _mixins.scss # Sass Mixins -| |– _helpers.scss # Class & placeholders helpers +| |– variables/ # Sass Variables used in Functions, Mixins, Helpers, ... +| | |- colors/ +| | | |- _base.scss +| | | |- _base-palette.scss # Base colors used everywhere +| | | |- _lifecycle-palette.scss # Colors used for lifecycle of an object (e.g. representing states such as new, frozen, done, ...), based on the base colors +| | | |- _semantic-palette.scss # Colors used for semantic meaning (e.g. red for errors, green for success, ...), based on the base colors +| | | ... +| | | +| | |- _depression.scss +| | |- _elevation.scss +| | |- _size.scss # Base sizes used everywhere (spacings, ...) +| | |- _spacing.scss +| | |- _typography.scss # Typography sizes, weights, families, ... +| | ... +| | +| |– functions/ # Sass Functions +| | |- _color.scss # Color manipulation functions +| | +| |– mixins/ # Sass Mixins +| |– helpers/ # Class & placeholders helpers | -|– vendors/ -| |– _bootstrap.scss # Bootstrap -| |– _jquery-ui.scss # jQuery UI -| ... # Etc… +|– vendors/ # Third-party libs, should be either: +| # - Overload of the lib SCSS variables (BEST way, but possible only if the lib exposes them. e.g. Bulma) +| # - Overload of the lib necessary CSS classes only (not great as it duplicates some rules in the browser, which add weight and computation. e.g. dataTables) +| # - Duplicate the lib CSS completly to insert SCSS variables (not great as it will be outdated when updating the lib itself. e.g. jQuery UI) +| |– _bulma-variables-overload.scss # Bulma CSS framework +| |– _jquery-ui.scss # jQuery UI +| ... # Etc… | |– base/ -| |– _reset.scss # Reset/normalize -| |– _typography.scss # Typography rules -| ... # Etc… +| |– _reset.scss # Reset/normalize +| |– _typography.scss # Typography fonts imports +| ... # Etc… | -|– components/ -| |– _buttons.scss # Buttons -| |– _carousel.scss # Carousel -| |– _cover.scss # Cover -| |– _dropdown.scss # Dropdown -| ... # Etc… +|– components/ # Components of the UI, each corresponding to a UI block and being usable as a standalone +| |– _button.scss +| |– _button-group.scss +| |– _global-search.scss +| |– _quick-create.scss +| ... | -|– layout/ -| |– _navigation.scss # Navigation -| |– _grid.scss # Grid system -| |– _header.scss # Header -| |– _footer.scss # Footer -| |– _sidebar.scss # Sidebar -| |– _forms.scss # Forms -| ... # Etc… +|– layout/ # Elements of the UI made of several components, making the layout of the app +| |– activity-panel/ +| |– dashboard/ +| |– object/ # DM object display (details, summary card, ...) +| |– tab-container/ +| ... | -|- *application/ # Elements that are not usable as a standalone (like componants and layouts are) and very application (the backoffice) specific +|- *application/ # Elements that are not usable as a standalone (like componants and layouts are) and very application (the backoffice) specific | |- display-block | |- tabular-fields | ... | -|- *datamodel/ # SCSS / CSS3 variables and CSS classes for PHP classes of the DM that are part of the core (not in a module) and cannot be styled otherwise +|- *datamodel/ # SCSS / CSS3 variables and CSS classes for *PHP* classes of the DM that are part of the core (not in a module) and cannot be styled otherwise +| |- _action.scss | |- _user.scss | ... | -|– pages/ -| |– _home.scss # Home specific styles -| |– _contact.scss # Contact specific styles -| ... # Etc… +|– pages/ # SCSS / CSS3 variables and CSS classes for HTML elements specific to backoffice pages +| |– _base.scss # Base for all backoffice pages +| |– _audit.scss # Audit page +| |– _csv-import.scss # CSV Import page +| ... # Etc… | -|- *blocks-integrations # Specific rules for the integration of a block with another one, those kind of rules should never be in the block partial directly -| |- _panel-with-datatable.scss # Changes the negative margins of the datatable so it overlaps the panel's original padding +|- *blocks-integrations # Specific rules for the integration of a UI block with another one, those kind of rules should NEVER be in the block partial directly +| |- alert/ +| | |- _alert-with-blocks.scss # How an alert should be displayed when after another block +| |- button/ +| | |- _button-with-button.scss # How a button should be displayed when after another button +| | |- _button-with-button-group.scss # How a button should be displayed when before/after a button group +| |- panel/ +| | |- _panel-with-blocks.scss # How a panel should be displayed when after another block +| | |- _panel-within-main-content.scss # How a panel becomes sticky when in the main content +| | |- _panel-within-modal.scss # How a panel becomes sticky when in a modal +| |- _tab-container-within-panel.scss # Changes the negative margins of the datatable so it overlaps the panel's original padding | ... | |– themes/ -| |– _theme.scss # Default theme -| |– _admin.scss # Admin theme -| ... # Etc… +| |– _page-banner.scss # ??? +| ... # Etc… | | -`- _shame.scss # Shame file, should contain all the ugly hacks (https://sass-guidelin.es/#shame-file) -`– main.scss # Main Sass file +|- _fallback.scss # Fallback file, should only contain rules that make standard HTML tags fallback to the style of a custom CSS class +|- _shame.scss # Shame file, should contain all the ugly hacks (https://sass-guidelin.es/#shame-file) +`– main.scss # Main Sass file ``` ## Usage @@ -84,7 +117,58 @@ To avoid common errors, files should be imported in the final file in the follow - Components - Layout - \*Application +- \*Datamodel - Pages - \*Block integrations - Themes -- Shame file \ No newline at end of file +- Shame file + +# BEM methodology +## Principles +[BEM is a methodology](https://getbem.com/) that helps you to create reusable components and code sharing in front‑end development. \ +The main idea is to use discriminant classes instead of nested basic selectors for 2 main reasons: + * It's easier to understand the purpose of a specific class when seeing it in the HTML markup of the SCSS file + * It's easier to override a specific class when needed as you don't need to use a selector at least as precise/complex as the one you want to override + +In our implementation, we start with the code of the UI block, followed by the sub-element, then the property or modifier. Separation is made of `--` instead of `__`. + +## Examples +### Classes and CSS properties example +```scss +// SCSS variables: +// - For CSS properties: CSS class, followed by CSS property +$ibo-button--padding-y: 6px !default; +$ibo-button--padding-x: 9px !default; +$ibo-button--border: 0 !default; +$ibo-button--border-radius: $ibo-border-radius-400 !default; +$ibo-button--box-shadow-bottom: 0px 2px 0px !default; +$ibo-button--box-shadow-top: inset 0px 2px 0px !default; + +$ibo-button--label--margin-left: $ibo-spacing-200 !default; + +// CSS classes: +.ibo-button { + padding: $ibo-button--padding-y $ibo-button--padding-x; + border: $ibo-button--border; + border-radius: $ibo-button--border-radius; +} + +.ibo-button--label { + margin-left: $ibo-button--label--margin-left; +} +``` + +### States example +```scss +// SCSS variables: +// Same rule as before, but with a `--is-` or `--on--` suffix +$ibo-quick-create--input--padding: $ibo-spacing-0 default; +$ibo-quick-create--input--padding-x--is-opened: $ibo-spacing-300 !default; +$ibo-quick-create--input--padding-y--is-opened: $ibo-spacing-300 !default; + +$ibo-quick-create--input--width: $ibo-size-0 !default; +$ibo-quick-create--input--width--is-opened: 245px !default; + +$ibo-quick-create--input--background-color: $ibo-color-white-100 !default; +$ibo-quick-create--input--background-color--on-hover: $ibo-color-grey-200 !default; +``` \ No newline at end of file diff --git a/css/backoffice/utils/variables/_depression.scss b/css/backoffice/utils/variables/_depression.scss index 2f233e7d6..308f1d69b 100644 --- a/css/backoffice/utils/variables/_depression.scss +++ b/css/backoffice/utils/variables/_depression.scss @@ -6,5 +6,5 @@ $ibo-depression-100: inset 0 1px 1px 0 rgba(0, 0, 0, 0.15) !default; :root{ - --ibo-elevation-100: #{$ibo-depression-100}; + --ibo-depression-100: #{$ibo-depression-100}; } \ No newline at end of file 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/public/js/bootstrap-patches.js b/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-patches.js index f4ae87988..b0319a110 100644 --- a/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-patches.js +++ b/datamodels/2.x/itop-portal-base/portal/public/js/bootstrap-patches.js @@ -31,7 +31,7 @@ $.fn.modal.Constructor.prototype.enforceFocus = function () { var $parent = $(e.target.parentNode); if ($modalElement[0] !== e.target && !$modalElement.has(e.target).length && !$parent.hasClass('ck-input')) { - e.target.focus() + $(e.target.activeElement).focus(); } }) }; 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..093e4c6f3 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,20 @@ class ObjectFormManager extends FormManager // Building the form foreach ($aFieldsAtts as $sAttCode => $iFieldFlags) { + // handle plugins fields + if(array_key_exists($sAttCode, $aFieldsExtraData) + && 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 +1011,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 +1024,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/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 869dc38cc..da10eb459 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -2259,7 +2259,7 @@ EOF $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); - $aParameters['class_category'] = $this->GetPropString($oField, 'class_category'); + $aParameters['class_category'] = $this->GetPropString($oField, 'class_category', ''); $aParameters['more_values'] = $this->GetPropString($oField, 'more_values', ''); $aParameters['depends_on'] = $sDependencies; }else { 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('