Merge branch 'develop' into SessionManagement

This commit is contained in:
Eric Espie
2021-09-07 16:33:10 +02:00
75 changed files with 1018 additions and 360 deletions

View File

@@ -19,8 +19,12 @@ use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Form\Form;
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\UI\Base\Component\Html\HtmlFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\SelectUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\MedallionIcon\MedallionIcon;
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\Title\Title;
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
@@ -2948,6 +2952,93 @@ EOF
}
}
/**
* Select the derived class to create
* @param string $sClass
* @param \WebPage $oP
* @param \ApplicationContext $oAppContext
* @param array $aPossibleClasses
* @param array $aHiddenFields
*
* @return void
* @throws \CoreException
* @throws \DictExceptionMissingString
*
* @since 3.0.0
*/
public static function DisplaySelectClassToCreate(string $sClass, WebPage $oP, ApplicationContext $oAppContext, array $aPossibleClasses, array $aHiddenFields)
{
$sClassLabel = MetaModel::GetName($sClass);
$sTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);
$oP->set_title($sTitle);
$sClassIconUrl = MetaModel::GetClassIcon($sClass, false);
$oPanel = PanelUIBlockFactory::MakeForClass($sClass, $sTitle)
->SetIcon($sClassIconUrl);
$oClassForm = FormUIBlockFactory::MakeStandard();
$oPanel->AddMainBlock($oClassForm);
$oClassForm->AddHtml($oAppContext->GetForForm())
->AddSubBlock(InputUIBlockFactory::MakeForHidden('checkSubclass', '0'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'new'));
foreach ($aHiddenFields as $sKey => $sValue) {
if (is_scalar($sValue)) {
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden($sKey, $sValue));
}
}
$aDefaults = utils::ReadParam('default', array(), false, 'raw_data');
foreach ($aDefaults as $key => $value) {
if (is_array($value)) {
foreach ($value as $key2 => $value2) {
if (is_array($value2)) {
foreach ($value2 as $key3 => $value3) {
$sValue = utils::EscapeHtml($value3);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key][$key2][$key3]", $sValue));
}
} else {
$sValue = utils::EscapeHtml($value2);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key][$key2]", $sValue));
}
}
} else {
$sValue = utils::EscapeHtml($value);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key]", $sValue));
}
}
$oClassForm->AddSubBlock(self::DisplayBlockSelectClassToCreate($sClass, $sClassLabel, $aPossibleClasses));
$oP->AddSubBlock($oPanel);
}
/**
* @param string $sClassLabel
* @param array $aPossibleClasses
* @param string $sClass
*
* @return UIContentBlock
* @throws \CoreException
*/
public static function DisplayBlockSelectClassToCreate( string $sClass, string $sClassLabel,array $aPossibleClasses): UIContentBlock
{
$oBlock= UIContentBlockUIBlockFactory::MakeStandard();
$oBlock->AddSubBlock(HtmlFactory::MakeRaw(Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel)));
$oSelect = SelectUIBlockFactory::MakeForSelect('class');
$oBlock->AddSubBlock($oSelect);
asort($aPossibleClasses);
foreach ($aPossibleClasses as $sClassName => $sClassLabel) {
$oSelect->AddOption(SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sClassLabel, ($sClassName == $sClass)));
}
$oToolbar = ToolbarUIBlockFactory::MakeForAction();
$oBlock->AddSubBlock($oToolbar);
$oToolbar->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), null, null, true));
return $oBlock;
}
/**
* @param \WebPage $oPage
* @param string $sClass

View File

@@ -543,7 +543,7 @@ EOF
if ($bFromDasboardPage) {
$sTitleForHTML = utils::HtmlEntities(Dict::S($this->sTitle));
$sHtml = "<div class=\"ibo-top-bar--toolbar-dashboard-title\">{$sTitleForHTML}</div>";
$sHtml = "<div class=\"ibo-top-bar--toolbar-dashboard-title\" title=\"{$sTitleForHTML}\">{$sTitleForHTML}</div>";
if ($oPage instanceof iTopWebPage) {
$oTopBar = $oPage->GetTopBarLayout();
$oToolbar = ToolbarUIBlockFactory::MakeStandard();

View File

@@ -4,6 +4,8 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlockUIBlockFactory;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
require_once(APPROOT.'/application/displayblock.class.inc.php');
@@ -220,6 +222,7 @@ class UIExtKeyWidget
$bAddingValue = true;
}
$sObjectImageAttCode = MetaModel::GetImageAttributeCode($sClassAllowed);
$bInitValue = false;
while ($oObj = $oAllowedValues->Fetch()) {
$aOption = [];
$aOption['value'] = $oObj->GetKey();
@@ -229,6 +232,7 @@ class UIExtKeyWidget
// When there is only once choice, select it by default
if ($value != $oObj->GetKey()) {
$value = $oObj->GetKey();
$bInitValue = true;
}
}
if ($oObj->IsObsolete()) {
@@ -263,10 +267,12 @@ class UIExtKeyWidget
oACWidget_{$this->iId}.emptyHtml = "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>$sMessage</p></div>";
oACWidget_{$this->iId}.AddSelectize('$sJsonOptions','$value');
$('#$this->iId').on('update', function() { oACWidget_{$this->iId}.Update(); } );
$('#$this->iId').on('change', function() { $(this).trigger('extkeychange') } );
$('#$this->iId').on('change', function() { $(this).trigger('extkeychange'); } );
EOF
);
if ($bInitValue) {
$oPage->add_ready_script("$('#$this->iId').one('validate', function() { $(this).trigger('change'); } );");
}
$sHTMLValue .= "<div class=\"ibo-input-select--action-buttons\">";
}
else
@@ -899,26 +905,17 @@ JS
}
}
$sDialogTitle = '';
$oPage->add('<div id="ac_create_'.$this->iId.'"><div class="wizContainer" style="vertical-align:top;"><div id="dcr_'.$this->iId.'">');
$oPage->add('<form>');
$sClassLabel = MetaModel::GetName($this->sTargetClass);
$oPage->add('<p>'.Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel));
$oPage->add('<nobr><select name="class">');
asort($aPossibleClasses);
foreach($aPossibleClasses as $sClassName => $sClassLabel)
{
$oPage->add("<option value=\"$sClassName\">$sClassLabel</option>");
}
$oPage->add('</select>');
$oPage->add('&nbsp; <button type="submit" class="action" style="margin-top:15px;"><span>' . Dict::S('UI:Button:Ok') . '</span></button></nobr></p>');
$sDialogTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);;
$oBlock = UIContentBlockUIBlockFactory::MakeStandard('ac_create_'.$this->iId,['ibo-is-visible']);
$oPage->AddSubBlock($oBlock);
$oClassForm = FormUIBlockFactory::MakeStandard();
$oBlock->AddSubBlock($oClassForm);
$oClassForm->AddSubBlock(cmdbAbstractObject::DisplayBlockSelectClassToCreate( $sClassLabel, $this->sTargetClass, $aPossibleClasses));
$oPage->add('</form>');
$oPage->add('</div></div></div>');
$oPage->add_ready_script("\$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("$('#dcr_{$this->iId} form').removeAttr('onsubmit');");
$oPage->add_ready_script("$('#dcr_{$this->iId} form').on('submit.uilinksWizard', oACWidget_{$this->iId}.DoSelectObjectClass);");
$oPage->add_ready_script("$('#ac_create_$this->iId').dialog({ width: 'auto', height: 'auto', maxHeight: $(window).height() - 50, autoOpen: false, modal: true, title: '$sDialogTitle'});\n");
$oPage->add_ready_script("$('#ac_create_{$this->iId} form').removeAttr('onsubmit');");
$oPage->add_ready_script("$('#ac_create_{$this->iId} form').on('submit.uilinksWizard', oACWidget_{$this->iId}.DoSelectObjectClass);");
}
/**

View File

@@ -911,9 +911,21 @@ class DeprecatedCallsLog extends LogAPI
$iStackCallerMethodLevel = $iStackDeprecatedMethodLevel + 1; // level 3 = caller of the deprecated method
if (array_key_exists($iStackCallerMethodLevel, $aStack)) {
$sCallerObject = $aStack[3]['class'];
$sCallerMethod = $aStack[3]['function'];
$sMessage .= " ({$sCallerObject}::{$sCallerMethod})";
$sCallerObject = $aStack[$iStackCallerMethodLevel]['class'] ?? null;
$sCallerMethod = $aStack[$iStackCallerMethodLevel]['function'] ?? null;
$sMessage .= ' (';
if (!is_null($sCallerObject)) {
$sMessage .= "{$sCallerObject}::{$sCallerMethod}";
} else {
$sCallerMethodFile = $aStack[$iStackCallerMethodLevel]['file'];
$sCallerMethodLine = $aStack[$iStackCallerMethodLevel]['line'];
if (!is_null($sCallerMethod)) {
$sMessage .= "call to {$sCallerMethod}() in {$sCallerMethodFile}#L{$sCallerMethodLine}";
} else {
$sMessage .= "{$sCallerMethodFile}#L{$sCallerMethodLine}";
}
}
$sMessage .= ')';
}
if (!empty($errstr)) {

View File

@@ -168,7 +168,6 @@ class ormCaseLog {
return $aEntries;
}
/**
* Returns a "plain text" version of the log (equivalent to $this->m_sLog) where all the HTML markup from the 'html' entries have been removed
* @return string
@@ -201,6 +200,15 @@ class ormCaseLog {
{
return ($this->m_sLog === null);
}
/**
* @return int The number of entries in this log
* @since 3.0.0
*/
public function GetEntryCount(): int
{
return count($this->m_aIndex);
}
public function ClearModifiedFlag()
{
@@ -570,7 +578,6 @@ class ormCaseLog {
$this->m_bModified = true;
}
public function AddLogEntryFromJSON($oJson, $bCheckUserId = true)
{
if (isset($oJson->user_id))
@@ -647,7 +654,6 @@ class ormCaseLog {
$this->m_bModified = true;
}
public function GetModifiedEntry($sFormat = 'text')
{
$sModifiedEntry = '';
@@ -726,4 +732,3 @@ class ormCaseLog {
return $sText;
}
}
?>

View File

@@ -16,6 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
*/
$ibo-breadcrumbs--margin-right: 16px !default;
$ibo-breadcrumbs--item--text-color: $ibo-color-grey-800 !default;
$ibo-breadcrumbs--item-icon--margin-x: 8px !default;
@@ -27,9 +29,27 @@ $ibo-breadcrumbs--item-label--max-width: 100px !default;
$ibo-breadcrumbs--item-separator--margin-x: 12px !default;
$ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
$ibo-breadcrumbs--previous-items-list-toggler--text-color: $ibo-color-grey-700 !default;
$ibo-breadcrumbs--previous-items-list-toggler--margin-right: 2 * $ibo-breadcrumbs--item-separator--margin-x !default;
$ibo-breadcrumbs--previous-items-list--top: 37px !default;
$ibo-breadcrumbs--previous-items-list--padding-y: 8px !default;
$ibo-breadcrumbs--previous-items-list--background-color: $ibo-color-white-100 !default;
$ibo-breadcrumbs--previous-item--text-color: $ibo-breadcrumbs--item--text-color !default;
$ibo-breadcrumbs--previous-item--padding-x: 12px !default;
$ibo-breadcrumbs--previous-item--padding-y: 12px !default;
$ibo-breadcrumbs--previous-item-label--max-width: 200px !default;
.ibo-breadcrumbs{
position: relative;
margin-right: $ibo-breadcrumbs--margin-right;
@extend %ibo-full-height-content;
&.ibo-is-overflowing {
justify-content: right;
}
* {
display: flex;
align-items: center;
@@ -40,13 +60,6 @@ $ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
@extend %ibo-font-ral-med-100;
&:not(:last-child){
&::after{
content: '\f054';
margin: 0 $ibo-breadcrumbs--item-separator--margin-x;
color: $ibo-breadcrumbs--item-separator--text-color;
@extend %fa-solid-base;
}
&:hover{
.ibo-breadcrumbs--item-icon{
> *{
@@ -78,3 +91,50 @@ $ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
max-width: $ibo-breadcrumbs--item-label--max-width;
@extend %ibo-text-truncated-with-ellipsis;
}
.ibo-breadcrumbs--item,
.ibo-breadcrumbs--previous-items-list-toggler {
&:not(:last-child){
&::after{
content: '\f054';
margin: 0 $ibo-breadcrumbs--item-separator--margin-x;
color: $ibo-breadcrumbs--item-separator--text-color;
@extend %fa-solid-base;
}
}
}
.ibo-breadcrumbs--previous-items-list-toggler {
margin-right: $ibo-breadcrumbs--previous-items-list-toggler--margin-right;
color: $ibo-breadcrumbs--previous-items-list-toggler--text-color !important;
@extend %ibo-font-ral-med-100;
&:not(:last-child) {
&::after {
position: absolute; /* To be outside of the button */
right: -1 * $ibo-breadcrumbs--previous-items-list-toggler--margin-right;
}
}
}
.ibo-breadcrumbs--previous-items-list {
display: flex;
flex-direction: column;
align-items: stretch; /* For the items to occupy the full width */
position: fixed;
top: $ibo-breadcrumbs--previous-items-list--top;
padding: $ibo-breadcrumbs--previous-items-list--padding-y 0;
background-color: $ibo-breadcrumbs--previous-items-list--background-color;
@extend %ibo-elevation-300;
}
.ibo-breadcrumbs--previous-item {
color: $ibo-breadcrumbs--previous-item--text-color;
@extend %ibo-font-ral-med-100;
padding: $ibo-breadcrumbs--previous-item--padding-y $ibo-breadcrumbs--previous-item--padding-x;
.ibo-breadcrumbs--item-label {
max-width: 200px;
}
}

View File

@@ -461,6 +461,7 @@ $ibo-button-colors: (
}
.ibo-button {
position: relative;
display: inline-block; /* Used to allow truncated text on .ibo-button--label */
padding: $ibo-button--padding-y $ibo-button--padding-x;
border: $ibo-button--border;

View File

@@ -36,11 +36,18 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
/* Avoid value to overflow from its container with very long strings (typically URLs) */
/* Note: Some types of attribute must be excluding as it can alter their rendering */
&:not([data-attribute-type="AttributeBlob"]):not([data-attribute-type="AttributeFile"]):not([data-attribute-type="AttributeImage"]):not([data-attribute-type="AttributeCustomFields"]):not(.ibo-input-file-select--container) {
/* We need the rule to apply for the class and all its descendants */
/* We need the rule to apply for the class and all its descendants, hence the "&, & *" */
.ibo-field--value {
* {
&, & * {
word-break: break-word;
white-space: pre-line;
white-space: inherit; /* Here we don't put break-spaces as it would put ".ibo-field-small .ibo-field-value" on a new line due to the spaces/indentation of the HTML templates. For now we rather have this rule than diminish the templates readability/maintenability */
}
}
&.ibo-field-large {
.ibo-field--value {
&, & * {
white-space: break-spaces; /* For large fields we don't have the issue stated above */
}
}
}
}
@@ -57,7 +64,6 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
.ibo-field--label {
position: relative; /* Necessary for fullscreen toggler */
display: flex;
justify-content: space-between;
align-items: center;
max-width: initial;
width: 100%;
@@ -113,7 +119,7 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
@extend %ibo-hyperlink-inherited-colors;
@extend %ibo-fully-centered-content;
@extend %ibo-font-size-100;
@extend %ibo-font-size-50;
&:hover {
background-color: $ibo-field--fullscreen-toggler--background-color--on-hover;
@@ -138,6 +144,9 @@ $ibo-field--value-decoration--spacing-x: 0.5rem !default;
}
}
}
.ibo-field--label-small .ibo-field--label{
width: 145px;
}
.ibo-field--value {
display: table;
width: 100%;

View File

@@ -26,6 +26,8 @@ $ibo-top-bar--elements-spacing: 32px !default;
$ibo-top-bar--quick-actions--margin-right: $ibo-top-bar--elements-spacing !default;
$ibo-top-bar--toolbar-dashboard-title--max-width: 350px !default;
/* CSS variables (can be changed directly from the browser) */
:root{
--ibo-top-bar--height: #{$ibo-top-bar--height};
@@ -65,6 +67,8 @@ $ibo-top-bar--quick-actions--margin-right: $ibo-top-bar--elements-spacing !defau
.ibo-top-bar--toolbar-dashboard-title {
@extend %ibo-font-size-250;
@extend %ibo-text-truncated-with-ellipsis;
max-width: $ibo-top-bar--toolbar-dashboard-title--max-width;
}
.ibo-top-bar--toolbar-dashboard-menu-toggler {

View File

@@ -202,17 +202,6 @@ $ibo-activity-panel--load-all-entries--is-hover--margin-left: ($ibo-activity-pan
flex-grow: 1; /* So it occupies all the remaining width, which is easier for the user to click */
word-break: break-word; /* To avoid content to overflow its container (typically very long URLs) */
/* Avoid pre (code snippets) to overflow outside the entry */
pre {
white-space: pre-line;
}
/* Avoid table to overflow outside the entry (see N°2127) */
table {
table-layout: fixed;
width: 100%;
}
/* Specific hyperlink color */
a {
color: $ibo-activity-entry--main-information-hyperlink--text-color;

View File

@@ -96,9 +96,12 @@ $ibo-activity-panel--add-caselog-entry-button--right: 12px !default;
$ibo-activity-panel--add-caselog-entry-button--top: 76px + $ibo-activity-panel--add-caselog-entry-button--right !default;
$ibo-activity-panel--add-caselog-entry-button--diameter: 36px !default;
$ibo-activity-panel--add-caselog-entry-button--background-color: $ibo-color-primary-600 !default;
$ibo-activity-panel--add-caselog-entry-button--background-color--on-hover: $ibo-color-primary-500 !default;
$ibo-activity-panel--add-caselog-entry-button--background-color--is-active: $ibo-color-primary-700 !default;
$ibo-activity-panel--add-caselog-entry-button--color: $ibo-color-white-100 !default;
$ibo-activity-panel--add-caselog-entry-button--border-radius: $ibo-border-radius-full !default;
$ibo-activity-panel--add-caselog-entry-button--box-shadow: $ibo-elevation-100 !default;
$ibo-activity-panel--add-caselog-entry-button--hover--box-shadow: $ibo-elevation-200 !default;
$ibo-activity-panel--add-caselog-entry-button--icon--height: 100% !default;
$ibo-activity-panel--add-caselog-entry-button--icon--width: $ibo-activity-panel--add-caselog-entry-button--icon--height !default;
@@ -379,6 +382,7 @@ $ibo-activity-panel--open-icon--margin-left: 0.75rem !default;
border-radius: $ibo-activity-panel--add-caselog-entry-button--border-radius;
box-shadow: $ibo-activity-panel--add-caselog-entry-button--box-shadow;
> i{
text-align: center;
height: $ibo-activity-panel--add-caselog-entry-button--icon--height;
@@ -388,7 +392,13 @@ $ibo-activity-panel--open-icon--margin-left: 0.75rem !default;
}
&:hover {
color: $ibo-activity-panel--add-caselog-entry-button--color;
}
background-color: $ibo-activity-panel--add-caselog-entry-button--background-color--on-hover;
box-shadow: $ibo-activity-panel--add-caselog-entry-button--hover--box-shadow;
}
&:active {
color: $ibo-activity-panel--add-caselog-entry-button--color;
background-color: $ibo-activity-panel--add-caselog-entry-button--background-color--is-active;
}
&.ibo-is-hidden{
display: none;
}

View File

@@ -137,6 +137,7 @@ $ibo-tab--temporary-remote-content--button--hover--color: $ibo-color-grey-200 !d
.ibo-tab-container--tab-container {
padding: $ibo-tab-container--tab-container--padding-y $ibo-tab-container--tab-container--padding-x;
overflow-x: auto;
}
.ibo-is-scrollable .ibo-tab-container--tab-container--label {

View File

@@ -1,17 +1,21 @@
$ibo-class-icon--small--size: 32px !default;
$ibo-class-icon--medium--size: 48px !default;
$ibo-class-icon--large--size: 64px !default;
.ibo-class-icon{
&.ibo-is-small{
width: 32px;
min-width: 32px;
max-height: 32px;
width: $ibo-class-icon--small--size;
min-width: $ibo-class-icon--small--size;
max-height: $ibo-class-icon--small--size;
}
&.ibo-is-medium{
width: 48px;
min-width: 48px;
max-height: 48px;
width: $ibo-class-icon--medium--size;
min-width: $ibo-class-icon--medium--size;
max-height: $ibo-class-icon--medium--size;
}
&.ibo-is-large{
width: 64px;
min-width: 64px;
max-height: 64px;
width: $ibo-class-icon--large--size;
min-width: $ibo-class-icon--large--size;
max-height: $ibo-class-icon--large--size;
}
}

View File

@@ -97,7 +97,7 @@ body.ibo-has-fullscreen-descendant {
.ibo-has-fullscreen-descendant {
position: static !important;
overflow: visible !important;
z-index: 9000 !important;
z-index: 1050 !important;
}
/* Used on a fullscreen element (see .ibo-has-fullscreen-descendant) */
@@ -110,7 +110,7 @@ body.ibo-has-fullscreen-descendant {
width: 100vw;
height: 100vh;
overflow: auto;
z-index: 9000;
z-index: 1050;
}
/****************/
@@ -159,8 +159,11 @@ body.ibo-has-fullscreen-descendant {
width: unset !important;
max-width: max-content;
}
code {
color: initial;
/* Preserve original text color in code blocks, except for the Highlight.js blocks which have their own colors */
& > code,
:not(pre.hljs) code {
color: inherit;
}
}

View File

@@ -24,4 +24,5 @@
@import "jquery-multiselect";
@import "datatables";
@import "jquery-treeview";
@import "jquery-blockui";
@import "jquery-blockui";
@import "magnific-popup";

View File

@@ -23,9 +23,12 @@ $ibo-vendors-datatables--page-length-selector--border-color: $ibo-color-grey-500
$ibo-vendors-datatables--page-length-selector--border-radius: $ibo-border-radius-300 !default;
$ibo-vendors-datatables--page-length-selector--border-color--on-focus: $ibo-color-primary-600 !default;
$ibo-vendors-datatables--cell--padding-x: 12px !default;
$ibo-vendors-datatables--cell--padding-y: 10px !default;
$ibo-vendors-datatables--column-sorting-icon--opacity: 0.3 !default;
$ibo-vendors-datatables--column-sorting-icon--opacity--is-sorted: 1 !default;
$ibo-vendors-datatables--column-sorting-icon--right: 1em !default;
$ibo-vendors-datatables--column-sorting-icon--right: calc((#{$ibo-vendors-datatables--cell--padding-x} - 8px) / 2) !default;
$ibo-vendors-datatables--column-sorting-icon--content: "\f0dc" !default;
$ibo-vendors-datatables--column-sorting-icon--content--is-sorted-asc: "\f0d8" !default;
$ibo-vendors-datatables--column-sorting-icon--content--is-sorted-desc: "\f0d7" !default;
@@ -137,7 +140,7 @@ $ibo-vendors-datatables--row-highlight--colors:(
.dataTable {
th, td {
position: relative;
padding: 10px 12px;
padding: $ibo-vendors-datatables--cell--padding-y $ibo-vendors-datatables--cell--padding-x;
@extend %ibo-font-ral-med-100; /* Necessary to set the font-size as its container can have a different one */
}

View File

@@ -0,0 +1,6 @@
.mfp-bg{
z-index: 1100;
}
.mfp-wrap{
z-index: 1101;
}

View File

@@ -179,7 +179,9 @@ a:hover {
.v-spacer {
padding-top: 1em;
}
#login-sso-buttons{
padding: 0 10px;
}
.sso-button {
margin: 5px 0px;
position: relative;

View File

@@ -170,10 +170,6 @@ JS
require_once(dirname(__FILE__).'/dbrestore.class.inc.php');
$sEnvironment = utils::ReadParam('environment', 'production', false, 'raw_data');
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
IssueLog::Info("Backup Restore - Acquiring the LOCK 'restore.$sEnvironment'");
$oRestoreMutex->Lock();
IssueLog::Info('Backup Restore - LOCK acquired, executing...');
try
{
set_time_limit(0);
@@ -203,7 +199,6 @@ JS
finally
{
unlink($tokenRealPath);
$oRestoreMutex->Unlock();
}
$oPage->output();

View File

@@ -30,9 +30,6 @@ if (!defined('APPROOT'))
}
}
require_once(APPROOT.'application/application.inc.php');
require_once(APPROOT.'application/webpage.class.inc.php');
require_once(APPROOT.'application/csvpage.class.inc.php');
require_once(APPROOT.'application/clipage.class.inc.php');
require_once(APPROOT.'application/ajaxwebpage.class.inc.php');
require_once(APPROOT.'core/log.class.inc.php');

View File

@@ -28,8 +28,8 @@ class DBRestore extends DBBackup
{
parent::__construct($oConfig);
$this->sDBUser = $oConfig->Get('db_user');
$this->sDBPwd = $oConfig->Get('db_pwd');
$this->sDBUser = $this->oConfig->Get('db_user');
$this->sDBPwd = $this->oConfig->Get('db_pwd');
}
protected function LogInfo($sMsg)
@@ -127,79 +127,88 @@ class DBRestore extends DBBackup
*/
public function RestoreFromCompressedBackup($sFile, $sEnvironment = 'production')
{
$this->LogInfo("Starting restore of ".basename($sFile));
$oRestoreMutex = new iTopMutex('restore.'.$sEnvironment);
IssueLog::Info("Backup Restore - Acquiring the LOCK 'restore.$sEnvironment'");
$oRestoreMutex->Lock();
$sNormalizedFile = strtolower(basename($sFile));
if (substr($sNormalizedFile, -4) == '.zip')
{
$this->LogInfo('zip file detected');
$oArchive = new ZipArchiveEx();
$oArchive->open($sFile);
}
elseif (substr($sNormalizedFile, -7) == '.tar.gz')
{
$this->LogInfo('tar.gz file detected');
$oArchive = new TarGzArchive($sFile);
}
else
{
throw new BackupException('Unsupported format for a backup file: '.$sFile);
}
try {
IssueLog::Info('Backup Restore - LOCK acquired, executing...');
$bReadonlyBefore = SetupUtils::EnterReadOnlyMode(MetaModel::GetConfig());
// Load the database
//
$sDataDir = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
try {
//safe zone for db backup => cron is stopped/ itop in readonly
$this->LogInfo("Starting restore of ".basename($sFile));
SetupUtils::builddir($sDataDir); // Here is the directory
$oArchive->extractTo($sDataDir);
$sDataFile = $sDataDir.'/itop-dump.sql';
$this->LoadDatabase($sDataFile);
$sNormalizedFile = strtolower(basename($sFile));
if (substr($sNormalizedFile, -4) == '.zip') {
$this->LogInfo('zip file detected');
$oArchive = new ZipArchiveEx();
$oArchive->open($sFile);
} elseif (substr($sNormalizedFile, -7) == '.tar.gz') {
$this->LogInfo('tar.gz file detected');
$oArchive = new TarGzArchive($sFile);
} else {
throw new BackupException('Unsupported format for a backup file: '.$sFile);
}
// Update the code
//
$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
// Load the database
//
$sDataDir = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
if (is_file($sDataDir.'/delta.xml'))
{
// Extract and rename delta.xml => <env>.delta.xml;
rename($sDataDir.'/delta.xml', $sDeltaFile);
}
else
{
@unlink($sDeltaFile);
}
if (is_dir(APPROOT.'data/production-modules/'))
{
try
{
SetupUtils::rrmdir(APPROOT.'data/production-modules/');
}
catch (Exception $e)
{
throw new BackupException("Can't remove production-modules dir", 0, $e);
SetupUtils::builddir($sDataDir); // Here is the directory
$oArchive->extractTo($sDataDir);
$sDataFile = $sDataDir.'/itop-dump.sql';
$this->LoadDatabase($sDataFile);
// Update the code
//
$sDeltaFile = APPROOT.'data/'.$sEnvironment.'.delta.xml';
if (is_file($sDataDir.'/delta.xml')) {
// Extract and rename delta.xml => <env>.delta.xml;
rename($sDataDir.'/delta.xml', $sDeltaFile);
} else {
@unlink($sDeltaFile);
}
if (is_dir(APPROOT.'data/production-modules/')) {
try {
SetupUtils::rrmdir(APPROOT.'data/production-modules/');
} catch (Exception $e) {
throw new BackupException("Can't remove production-modules dir", 0, $e);
}
}
if (is_dir($sDataDir.'/production-modules')) {
rename($sDataDir.'/production-modules', APPROOT.'data/production-modules/');
}
$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
rename($sDataDir.'/config-itop.php', $sConfigFile);
@chmod($sConfigFile, 0440); // Read-only
try {
SetupUtils::rrmdir($sDataDir);
} catch (Exception $e) {
throw new BackupException("Can't remove data dir", 0, $e);
}
$oEnvironment = new RunTimeEnvironment($sEnvironment);
$oEnvironment->CompileFrom($sEnvironment);
} finally {
if (! $bReadonlyBefore) {
SetupUtils::ExitReadOnlyMode();
} else {
//we are in the scope of main process that needs to handle/keep readonly mode.
$this->LogInfo("Keep readonly mode after restore");
}
}
}
if (is_dir($sDataDir.'/production-modules'))
finally
{
rename($sDataDir.'/production-modules', APPROOT.'data/production-modules/');
IssueLog::Info('Backup Restore - LOCK released.');
$oRestoreMutex->Unlock();
}
$sConfigFile = APPROOT.'conf/'.$sEnvironment.'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
rename($sDataDir.'/config-itop.php', $sConfigFile);
@chmod($sConfigFile, 0440); // Read-only
try
{
SetupUtils::rrmdir($sDataDir);
}
catch (Exception $e)
{
throw new BackupException("Can't remove data dir", 0, $e);
}
$oEnvironment = new RunTimeEnvironment($sEnvironment);
$oEnvironment->CompileFrom($sEnvironment);
}
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<DatabaseSchema alias="DatabaseSchema" id="20">
<name>openerpprod</name>
<name>odooprod</name>
<description></description>
<org_id>2</org_id>
<business_criticity>low</business_criticity>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<OSVersion alias="OSVersion" id="8">
<name>Unbuntu 11.10</name>
<name>Ubuntu 20.04</name>
<osfamily_id>7</osfamily_id>
</OSVersion>
<OSVersion alias="OSVersion" id="4">
<name>Windows 2008 Server</name>
<name>Windows 2019 Server</name>
<osfamily_id>3</osfamily_id>
</OSVersion>
</Set>

View File

@@ -90,8 +90,8 @@
<brand_id>1</brand_id>
<model_id>4</model_id>
<asset_number></asset_number>
<purchase_date>2011-05-22</purchase_date>
<end_of_warranty>2013-05-21</end_of_warranty>
<purchase_date>2021-07-30</purchase_date>
<end_of_warranty>2025-07-29</end_of_warranty>
<rack_id>0</rack_id>
<enclosure_id>0</enclosure_id>
<nb_u></nb_u>

View File

@@ -3,13 +3,13 @@
<Software alias="Software" id="2">
<name>MySql</name>
<vendor>Oracle</vendor>
<version>5.3</version>
<version>8</version>
<type>DBServer</type>
</Software>
<Software alias="Software" id="1">
<name>Oracle DB engine</name>
<vendor>Oracle</vendor>
<version>11i</version>
<version>19c</version>
<type>DBServer</type>
</Software>
</Set>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<WebApplication alias="WebApplication" id="25">
<name>Open ERP</name>
<name>Odoo</name>
<description></description>
<org_id>2</org_id>
<business_criticity>low</business_criticity>

View File

@@ -703,6 +703,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL dotaz',
'UI:RunQuery:HeaderOQLExpression+' => 'Dotaz v OQL syntaxi',
'UI:RunQuery:ExpressionToEvaluate' => 'Dotaz k vyhodnocení: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Více informací o dotazu: ',
'UI:RunQuery:DevelopedQuery' => 'Rekonstruovaný dotaz: ',
'UI:RunQuery:SerializedFilter' => 'Serializovaný filtr: ',

View File

@@ -692,6 +692,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL Udtryk',
'UI:RunQuery:HeaderOQLExpression+' => 'Forespørgslen i OQL syntaks',
'UI:RunQuery:ExpressionToEvaluate' => 'Udtryk der skal evalueres: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Mere information om forespørgslen: ',
'UI:RunQuery:DevelopedQuery' => 'Videreudviklet forespørgselsudtryk: ',
'UI:RunQuery:SerializedFilter' => 'Serielt filter: ',

View File

@@ -692,6 +692,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL-Ausdruck',
'UI:RunQuery:HeaderOQLExpression+' => 'Die Abfrage in OQL-Syntax',
'UI:RunQuery:ExpressionToEvaluate' => 'Auszuwertender Ausdruck: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Mehr Informationen zur Abfrage: ',
'UI:RunQuery:DevelopedQuery' => 'Überarbeiteter Abfrageausdruck: ',
'UI:RunQuery:SerializedFilter' => 'Serieller Filter: ',

View File

@@ -720,6 +720,7 @@ We hope youll enjoy this version as much as we enjoyed imagining and creating
'UI:RunQuery:HeaderOQLExpression' => 'OQL Expression',
'UI:RunQuery:HeaderOQLExpression+' => 'The query in OQL syntax',
'UI:RunQuery:ExpressionToEvaluate' => 'Expression to evaluate: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments',
'UI:RunQuery:MoreInfo' => 'More information about the query: ',
'UI:RunQuery:DevelopedQuery' => 'Redevelopped query expression: ',
'UI:RunQuery:SerializedFilter' => 'Serialized filter: ',

View File

@@ -703,6 +703,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array(
'UI:RunQuery:HeaderOQLExpression' => 'Expresión OQL',
'UI:RunQuery:HeaderOQLExpression+' => 'La consulta en sintáxis OQL',
'UI:RunQuery:ExpressionToEvaluate' => 'Expresión a evaluar: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Más información acerca de la consulta: ',
'UI:RunQuery:DevelopedQuery' => 'Expresión de consulta rediseñada: ',
'UI:RunQuery:SerializedFilter' => 'Filtro de serialización: ',

View File

@@ -703,6 +703,7 @@ Nous espérons que vous aimerez cette version autant que nous avons eu du plaisi
'UI:RunQuery:HeaderOQLExpression' => 'Requête OQL',
'UI:RunQuery:HeaderOQLExpression+' => 'La requête en OQL',
'UI:RunQuery:ExpressionToEvaluate' => 'Requête à exécuter : ',
'UI:RunQuery:QueryArguments' => 'Arguments de la requête',
'UI:RunQuery:MoreInfo' => 'Plus d\'information sur la requête : ',
'UI:RunQuery:DevelopedQuery' => 'Requête OQL décompilée : ',
'UI:RunQuery:SerializedFilter' => 'Version sérialisée : ',

View File

@@ -692,6 +692,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL kifejezés',
'UI:RunQuery:HeaderOQLExpression+' => '',
'UI:RunQuery:ExpressionToEvaluate' => 'Értékelendő kifejezés: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Több információ a lekérdezésről: ',
'UI:RunQuery:DevelopedQuery' => 'Újraírt lekérdező értékelés: ',
'UI:RunQuery:SerializedFilter' => 'Szerializált szűrő: ',

View File

@@ -703,6 +703,7 @@ Dict::Add('IT IT', 'Italian', 'Italiano', array(
'UI:RunQuery:HeaderOQLExpression' => 'Espressioni OQL',
'UI:RunQuery:HeaderOQLExpression+' => '',
'UI:RunQuery:ExpressionToEvaluate' => 'Espressione da valutare: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Maggiori informazioni sulla query: ',
'UI:RunQuery:DevelopedQuery' => 'Espressione della query riqualificata:',
'UI:RunQuery:SerializedFilter' => 'Filtro pubblicato: ',

View File

@@ -692,6 +692,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL式',
'UI:RunQuery:HeaderOQLExpression+' => 'OQL文法によるクエリ',
'UI:RunQuery:ExpressionToEvaluate' => '評価式',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'クエリに関する追加情報',
'UI:RunQuery:DevelopedQuery' => 'クエリ式の再開発',
'UI:RunQuery:SerializedFilter' => 'シリアライズされたフィルタ:',

View File

@@ -703,6 +703,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL-expressie',
'UI:RunQuery:HeaderOQLExpression+' => 'De query in OQL syntax',
'UI:RunQuery:ExpressionToEvaluate' => 'Expressie om te evalueren: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Meer informatie over de query: ',
'UI:RunQuery:DevelopedQuery' => 'Herschreven query-expressie: ',
'UI:RunQuery:SerializedFilter' => 'Geserialiseerde filter: ',

View File

@@ -705,6 +705,7 @@ Dict::Add('PL PL', 'Polish', 'Polski', array(
'UI:RunQuery:HeaderOQLExpression' => 'Wyrażenie OQL',
'UI:RunQuery:HeaderOQLExpression+' => 'Zapytanie w składni OQL',
'UI:RunQuery:ExpressionToEvaluate' => 'Wyrażenie do wykonania: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Więcej informacji o zapytaniu: ',
'UI:RunQuery:DevelopedQuery' => 'Rozwinięte wyrażenie zapytania: ',
'UI:RunQuery:SerializedFilter' => 'Filtr serializowany: ',

View File

@@ -703,6 +703,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array(
'UI:RunQuery:HeaderOQLExpression' => 'A consulta na sintaxe OQL',
'UI:RunQuery:HeaderOQLExpression+' => 'A consulta na sintaxe OQL',
'UI:RunQuery:ExpressionToEvaluate' => 'Expressão para avaliar: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Mais informações sobre a consulta: ',
'UI:RunQuery:DevelopedQuery' => 'Redevelopped query expression: ',
'UI:RunQuery:SerializedFilter' => 'Filtro serializado: ',

View File

@@ -704,6 +704,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array(
'UI:RunQuery:HeaderOQLExpression' => 'Выражение OQL',
'UI:RunQuery:HeaderOQLExpression+' => 'Запрос в синтаксисе OQL',
'UI:RunQuery:ExpressionToEvaluate' => 'Оценка выражения: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Подробная информация о запросе: ',
'UI:RunQuery:DevelopedQuery' => 'Декомпилированный запрос OQL: ',
'UI:RunQuery:SerializedFilter' => 'Сериализованная версия: ',

View File

@@ -693,6 +693,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL Výraz',
'UI:RunQuery:HeaderOQLExpression+' => '',
'UI:RunQuery:ExpressionToEvaluate' => 'Výraz k určeniu: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Viac informácií o dopyte: ',
'UI:RunQuery:DevelopedQuery' => 'Dopyt rozvinutého výrazu: ',
'UI:RunQuery:SerializedFilter' => 'Serializovaný filter: ',

View File

@@ -703,6 +703,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL ifadesi',
'UI:RunQuery:HeaderOQLExpression+' => 'OQL yapısında sorgu',
'UI:RunQuery:ExpressionToEvaluate' => 'Değerlendirilecek ifade: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => 'Sorgu hakkında detaylı bilgi: ',
'UI:RunQuery:DevelopedQuery' => 'Yeniden düzenlenen sorgu: ',
'UI:RunQuery:SerializedFilter' => 'Özel filtre: ',

View File

@@ -0,0 +1,23 @@
<?php
/**
* Copyright (C) 2013-2021 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
*/
// Global search
Dict::Add('EN US', 'English', 'English', array(
'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label' => 'Previous pages',
));

View File

@@ -0,0 +1,23 @@
<?php
/**
* Copyright (C) 2013-2021 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
*/
// Global search
Dict::Add('FR FR', 'French', 'Français', array(
'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label' => 'Pages précédentes',
));

View File

@@ -703,6 +703,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array(
'UI:RunQuery:HeaderOQLExpression' => 'OQL 表达式',
'UI:RunQuery:HeaderOQLExpression+' => 'OQL 语法表示的查询',
'UI:RunQuery:ExpressionToEvaluate' => '请输入表达式: ',
'UI:RunQuery:QueryArguments' => 'Query Arguments~~',
'UI:RunQuery:MoreInfo' => '该查询的更多信息: ',
'UI:RunQuery:DevelopedQuery' => '重新开发的查询表达式: ',
'UI:RunQuery:SerializedFilter' => '序列化的过滤器: ',

View File

@@ -30,6 +30,26 @@ $(function()
new_entry: null,
max_count: 8
},
css_classes:
{
is_hidden: 'ibo-is-hidden',
is_transparent: 'ibo-is-transparent',
is_opaque: 'ibo-is-opaque',
is_overflowing: 'ibo-is-overflowing',
breadcrumbs_item: 'ibo-breadcrumbs--item',
breadcrumbs_previous_item: 'ibo-breadcrumbs--previous-item',
},
js_selectors:
{
breadcrumbs: '[data-role="ibo-breadcrumbs"]',
item: '[data-role="ibo-breadcrumbs--item"]',
previous_items_container: '[data-role="ibo-breadcrumbs--previous-items-container"]',
previous_items_list_toggler: '[data-role="ibo-breadcrumbs--previous-items-list-toggler"]',
previous_items_list: '[data-role="ibo-breadcrumbs--previous-items-list"]',
previous_item: '[data-role="ibo-breadcrumbs--previous-item"]',
},
items_observer: null,
// the constructor
_create: function()
@@ -41,7 +61,7 @@ $(function()
// Check that storage API is available
if(typeof(Storage) !== 'undefined')
{
$(window).bind('hashchange', function(e)
$(window).on('hashchange', function(e)
{
me.RefreshLatestEntry();
});
@@ -50,7 +70,7 @@ $(function()
if (this.options.new_entry !== null) {
var sUrl = this.options.new_entry.url;
if (sUrl.length == 0) {
if (sUrl.length === 0) {
sUrl = window.location.href;
}
// Eliminate items having the same id, before appending the new item
@@ -71,6 +91,8 @@ $(function()
}
this._writeDataToStorage(aBreadCrumb);
// Build markup
// - Add entries to the markup
for (iEntry in aBreadCrumb)
{
var sBreadcrumbsItemHtml = '';
@@ -90,30 +112,131 @@ $(function()
var sTitle = oEntry['description'],
sLabel = oEntry['label'];
if (sTitle.length == 0) {
if (sTitle.length === 0) {
sTitle = sLabel;
}
sTitle = EncodeHtml(sTitle, false);
sLabel = EncodeHtml(sLabel, false);
if ((this.options.new_entry !== null) && (iEntry == aBreadCrumb.length - 1)) {
if ((this.options.new_entry !== null) && (iEntry === aBreadCrumb.length - 1)) {
// Last entry is the current page
sBreadcrumbsItemHtml += '<span class="ibo-breadcrumbs--item--is-current" data-breadcrumb-entry-number="'+iEntry+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></span>';
sBreadcrumbsItemHtml += '<span class="ibo-is-current" data-role="" data-breadcrumb-entry-number="'+iEntry+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></span>';
} else {
var sSanitizedUrl = StripArchiveArgument(oEntry['url']);
sBreadcrumbsItemHtml += '<a class="ibo-breadcrumbs--item" data-breadcrumb-entry-number="'+iEntry+'" href="'+sSanitizedUrl+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></a>';
sBreadcrumbsItemHtml += '<a class="" data-role="" data-breadcrumb-entry-number="'+iEntry+'" href="'+sSanitizedUrl+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></a>';
}
}
this.element.append(sBreadcrumbsItemHtml);
const oNormalItemElem = $(sBreadcrumbsItemHtml)
.addClass(this.css_classes.breadcrumbs_item)
.attr('data-role', 'ibo-breadcrumbs--item');
this.element.append(oNormalItemElem);
const oPreviousItemElem = $(sBreadcrumbsItemHtml)
.addClass(this.css_classes.breadcrumbs_previous_item)
.attr('data-role', 'ibo-breadcrumbs--previous-item')
// Note: We prepend items as we want the oldest to be at the bottom of the list, like in a browser
this.element.find(this.js_selectors.previous_items_list).prepend(oPreviousItemElem);
}
}
this._updateOverflowingState();
this._bindEvents();
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
// Remove listeners
this.element.find(this.js_selectors.previous_items_list_toggler).off('click');
// Remove observers
this.items_observer.disconnect();
// Clear any existing entries in the markup
this.element.find(this.js_selectors.item).remove();
this.element.find(this.js_selectors.previous_item).remove();
this.element.removeClass('ibo-breadcrumbs');
},
_bindEvents: function ()
{
const me = this;
// Enable responsiveness if more than 1 item
if(window.IntersectionObserver && (this.element.find(this.js_selectors.item).length > 1)) {
// Set an observer on the items
this.items_observer = new IntersectionObserver(function(aItems, oIntersectObs){
aItems.forEach(oItem => {
let oItemElem = $(oItem.target);
let bIsVisible = oItem.isIntersecting;
// Important: We toggle "visibility" instead of "display" otherwise once they are hidden, they never trigger back the intersection.
if(bIsVisible) {
oItemElem.css('visibility', '');
}
else {
// Here we also check if the item has an invisible left sibbling before hiding it.
// There reason is that on initialization, the last item might be overflowing on the right BEFORE the breadcrumbs is flagged as overflowing, making it disappear
let oLeftSiblingElem = oItemElem.prev(me.js_selectors.item);
if (oLeftSiblingElem.length > 0 && oLeftSiblingElem.css('visibility') !== 'hidden') {
bIsVisible = true;
} else {
oItemElem.css('visibility', 'hidden');
}
}
me._updateItemDisplay(oItemElem, bIsVisible);
});
let bShouldShowPreviousItemsList = false;
me.element.find(me.js_selectors.item).each(function() {
if ($(this).css('visibility') === 'hidden') {
bShouldShowPreviousItemsList = true;
// Note: Can break a .each function loop, must return false
return false;
}
});
// Move previous items toggler before first visible item for a better UX
if (bShouldShowPreviousItemsList) {
let oFirstVisibleItem = me.element.find(me.js_selectors.item).first();
me.element.find(me.js_selectors.item).each(function() {
if ($(this).css('visibility') !== 'hidden') {
oFirstVisibleItem = $(this);
// Note: Can break a .each function loop, must return false
return false;
}
});
me.element.find(me.js_selectors.previous_items_container).insertBefore(oFirstVisibleItem);
}
me._updateOverflowingState();
me._updatePreviousItemsList();
}, {
root: $(this.js_selectors.breadcrumbs)[0],
threshold: [1] // Must be completely visible
});
this.element.find(this.js_selectors.item).each(function(){
me.items_observer.observe(this);
});
this.element.find(this.js_selectors.previous_items_list_toggler).on('click', function (oEvent) {
oEvent.preventDefault();
me.element.find(me.js_selectors.previous_items_list).toggleClass(me.css_classes.is_hidden);
});
$('body').on('click', function (oEvent) {
if (true === me.element.find(me.js_selectors.previous_items_list).hasClass(me.css_classes.is_hidden)) {
return;
}
if ($(oEvent.target.closest(me.js_selectors.previous_items_container)).length === 0) {
me.element.find(me.js_selectors.previous_items_list).addClass(me.css_classes.is_hidden);
}
});
}
},
_readDataFromStorage: function()
{
var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
@@ -131,6 +254,7 @@ $(function()
sBreadCrumbData = JSON.stringify(aBreadCrumb);
sessionStorage.setItem(sBreadCrumbStorageKey, sBreadCrumbData);
},
// Refresh the latest entry (navigating to a tab)
RefreshLatestEntry: function(sRefreshHrefTo)
{
@@ -149,5 +273,68 @@ $(function()
}
this._writeDataToStorage(aBreadCrumb);
},
// Helpers
/**
* Update item display based on its visibility to the user
*
* @param oItemElem {Object} jQuery element
* @param bIsVisible {boolean|null} If null, visibility will be computed automatically. Not that performance might not be great so it's preferable to pass the value when known
* @return {void}
* @private
*/
_updateItemDisplay(oItemElem, bIsVisible = null)
{
const iEntryNumber = parseInt(oItemElem.attr('data-breadcrumb-entry-number'));
const oMatchingExtraItemElem = this.element.find(this.js_selectors.previous_items_list+' [data-breadcrumb-entry-number="'+iEntryNumber+'"]');
// Manually check if the item is visible if the info isn't passed
if (bIsVisible === null) {
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oItemElem[0], true, 2);
}
// Hide/show the corresponding extra item element
if (bIsVisible) {
oMatchingExtraItemElem.addClass(this.css_classes.is_hidden);
} else {
oMatchingExtraItemElem.removeClass(this.css_classes.is_hidden);
}
},
/**
* Update previous items list
*
* @return {void}
* @private
*/
_updatePreviousItemsList: function () {
const iVisiblePreviousItemsCount = this.element.find(this.js_selectors.previous_item+':not(.'+this.css_classes.is_hidden+')').length;
const oPreviousItemsContainerElem = this.element.find(this.js_selectors.previous_items_container);
if (iVisiblePreviousItemsCount > 0) {
oPreviousItemsContainerElem.removeClass(this.css_classes.is_hidden);
} else {
oPreviousItemsContainerElem.addClass(this.css_classes.is_hidden);
}
},
/**
* Update the overflowing state of the breadcrumbs by checking if the items cumulated width is greater than the breadcrumbs visible space
*
* @return {void}
* @private
*/
_updateOverflowingState: function () {
const fBreadcrumbsWidth = this.element.outerWidth();
let fItemsTotalWidth = 0;
this.element.find(this.js_selectors.item).each(function () {
fItemsTotalWidth += $(this).outerWidth();
});
if (fItemsTotalWidth > fBreadcrumbsWidth) {
this.element.addClass(this.css_classes.is_overflowing);
} else {
this.element.removeClass(this.css_classes.is_overflowing);
}
}
});
});

View File

@@ -27,7 +27,7 @@ $(function()
this.moveup_btn = $('<button type="button" disabled class="ibo-button ibo-is-regular ibo-is-neutral ibo-button--vertical-align">'+this.options.labels.moveup+'</button>');
this.movedown_btn = $('<button type="button" disabled class="ibo-button ibo-is-regular ibo-is-neutral ibo-button--vertical-align">'+this.options.labels.movedown+'</button>');
columnWithButtons = $('<div class="ibo-mini-column"></div>');
this.element.parent().parent().append(columnWithButtons.append(this.moveup_btn).append('<br>').append(this.movedown_btn));
this.element.parent().parent().append(columnWithButtons.append(this.moveup_btn).append(this.movedown_btn));
this.moveup_btn.on('click', function () {
me._moveUp();
});
@@ -93,9 +93,10 @@ $(function()
{
this.element
.removeClass('itop-fieldsorter');
let oButtonsColumn = this.moveup_btn.parent('.ibo-mini-column');
this.moveup_btn.remove();
this.movedown_btn.remove();
oButtonsColumn.remove();
this.element.sortable('destroy').html('');
},
// _setOptions is called with a hash of all options that are changing

View File

@@ -787,6 +787,22 @@ $(function()
return oEntries;
},
/**
* @returns {Object} The case logs having a new entry and their values, format is {<ATT_CODE_1>: <HTML_VALUE_1>, <ATT_CODE_2>: <HTML_VALUE_2>}
* @private
*/
_GetExtraInputsFromAllForms: function () {
const me = this;
let oExtraInputs = {};
this.element.find(this.js_selectors.caselog_entry_form).each(function () {
const oEntryFormElem = $(this);
oExtraInputs = $.extend(oExtraInputs, oEntryFormElem.triggerHandler('get_extra_inputs.caselog_entry_form.itop'));
});
return oExtraInputs;
},
/**
* @return {boolean} True if at least 1 of the entry form is draft (has some text in it)
* @private
@@ -863,6 +879,7 @@ $(function()
_SendEntriesToServer: function (sStimulusCode = null) {
const me = this;
const oEntries = this._GetEntriesFromAllForms();
const oExtraInputs = this._GetExtraInputsFromAllForms();
// Proceed only if entries to send
if (Object.keys(oEntries).length === 0) {
@@ -870,13 +887,13 @@ $(function()
}
// Prepare parameters
let oParams = {
let oParams = $.extend(oExtraInputs, {
operation: 'activity_panel_add_caselog_entries',
object_class: this._GetHostObjectClass(),
object_id: this._GetHostObjectID(),
transaction_id: this.options.transaction_id,
entries: oEntries,
};
});
// Freeze case logs
this._FreezeCaseLogsEntryForms();

View File

@@ -157,6 +157,9 @@ $(function() {
this.element.on('get_entry.caselog_entry_form.itop', function () {
return me._GetInputData();
});
this.element.on('get_extra_inputs.caselog_entry_form.itop', function () {
return me._GetExtraInputs();
});
// Clear the entry value
this.element.on('clear_entry.caselog_entry_form.itop', function () {
me._EmptyInput();
@@ -281,6 +284,27 @@ $(function() {
_GetInputData: function() {
return (this._GetCKEditorInstance() === undefined) ? '' : this._GetCKEditorInstance().getData();
},
_GetExtraInputs: function() {
let aExtraInputs = {};
const aFormInputs = this.element.serializeArray();
// Iterate across all values that would be sent if we submit current form
for (const aExtraInput of aFormInputs) {
// If we don't already have a value with the same name, add it
// Otherwise we'll consider that we need to return this value as an array of values
if(aExtraInputs[aExtraInput.name] === undefined) {
aExtraInputs[aExtraInput.name] = aExtraInput.value;
}
else {
if(Array.isArray(aExtraInputs[aExtraInput.name])){
aExtraInputs[aExtraInput.name].push(aExtraInput.value);
}
else{
aExtraInputs[aExtraInput.name] = [aExtraInputs[aExtraInput.name], aExtraInput.value];
}
}
};
return aExtraInputs;
},
// - Main actions
_ShowMainActions: function() {
this.element.find(this.js_selectors.main_actions).removeClass(this.css_classes.is_hidden);

View File

@@ -224,7 +224,6 @@ $(function()
$('#ibo-breadcrumbs')
.breadcrumbs('destroy')
.html('')
.breadcrumbs({
itop_instance_id: oData['breadcrumb_instance_id'],
max_count: oData['breadcrumb_max_count'],

View File

@@ -8,12 +8,8 @@ use Combodo\iTop\Application\Helper\Session;
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Form\Form;
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\GlobalSearch\GlobalSearchHelper;
use Combodo\iTop\Application\UI\Base\Component\Html\HtmlFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\InputUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Input\SelectUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper;
use Combodo\iTop\Application\UI\Base\Component\Title\Title;
@@ -753,54 +749,7 @@ EOF
cmdbAbstractObject::DisplayCreationForm($oP, $sRealClass, $oObjToClone, array(), array('wizard_container' => 1, 'keep_source_object' => true)); // wizard_container: Display the title above the form
} else {
// Select the derived class to create
$sClassLabel = MetaModel::GetName($sClass);
$sClassIconUrl = MetaModel::GetClassIcon($sClass, false);
$sTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);
$oP->set_title($sTitle);
$oPanel = PanelUIBlockFactory::MakeForClass($sClass, $sTitle)
->SetIcon($sClassIconUrl);
$oP->AddUiBlock($oPanel);
$oClassForm = FormUIBlockFactory::MakeStandard();
$oPanel->AddMainBlock($oClassForm);
$oClassForm->AddSubBlock(HtmlFactory::MakeParagraph(Dict::Format('UI:SelectTheTypeOf_Class_ToCreate', $sClassLabel)))
->AddHtml($oAppContext->GetForForm())
->AddSubBlock(InputUIBlockFactory::MakeForHidden('checkSubclass', '0'))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('state', $sStateCode))
->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'new'));
$aDefaults = utils::ReadParam('default', array(), false, 'raw_data');
foreach ($aDefaults as $key => $value) {
if (is_array($value)) {
foreach ($value as $key2 => $value2) {
if (is_array($value2)) {
foreach ($value2 as $key3 => $value3) {
$sValue = utils::EscapeHtml($value3);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key][$key2][$key3]", $sValue));
}
} else {
$sValue = utils::EscapeHtml($value2);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key][$key2]", $sValue));
}
}
} else {
$sValue = utils::EscapeHtml($value);
$oClassForm->AddSubBlock(InputUIBlockFactory::MakeForHidden("default[$key]", $sValue));
}
}
$oSelect = SelectUIBlockFactory::MakeForSelect('class');
$oClassForm->AddSubBlock($oSelect);
asort($aPossibleClasses);
foreach ($aPossibleClasses as $sClassName => $sClassLabel) {
$oSelect->AddOption(SelectOptionUIBlockFactory::MakeForSelectOption($sClassName, $sClassLabel, ($sClassName == $sClass)));
}
$oToolbar = ToolbarUIBlockFactory::MakeForAction();
$oClassForm->AddSubBlock($oToolbar);
$oToolbar->AddSubBlock(ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), null, null, true));
cmdbAbstractObject::DisplaySelectClassToCreate($sClass, $oP, $oAppContext, $aPossibleClasses,['state' => $sStateCode]);
}
break;

View File

@@ -1369,7 +1369,7 @@ EOF
foreach($aLicenses as $oLicense)
{
$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<a id="toggle_'.$index.'" class="CollapsibleLabel" style="cursor:pointer;">Details</a>)');
$oPage->add('<div id="license_'.$index.'" class="license_text" style="display:none;overflow:auto;max-height:10em;font-size:small;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;">'.$oLicense->text.'</div>');
$oPage->add('<div id="license_'.$index.'" class="license_text ibo-is-html-content" style="display:none;overflow:auto;max-height:10em;font-size:small;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;">'.$oLicense->text.'</div>');
$oPage->add_ready_script(<<<JS
$("#toggle_$index").on('click', function() {
$(this).toggleClass('open');

View File

@@ -1009,7 +1009,7 @@ EOF
if ($(this).val() == sRefValue)
{
iOccurences++;
}c
}
});
if ((iOccurences > 1) && (sRefValue != ':none:'))
{

View File

@@ -8,6 +8,7 @@ use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSection;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Field\Field;
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSet;
use Combodo\iTop\Application\UI\Base\Component\Form\Form;
use Combodo\iTop\Application\UI\Base\Component\Form\FormUIBlockFactory;
@@ -163,14 +164,15 @@ try
}
}
$oPanelQuery = PanelUIBlockFactory::MakeWithBrandingPrimaryColor(Dict::S('UI:RunQuery:ExpressionToEvaluate'));
$oP->AddSubBlock($oPanelQuery);
$oQueryForm = new Form();
$oP->AddUiBlock($oQueryForm);
$oPanelQuery->AddSubBlock($oQueryForm);
$oHiddenParams = new Html($oAppContext->GetForForm());
$oQueryForm->AddSubBlock($oHiddenParams);
//--- Query textarea
$oQueryForm->AddSubBlock(TitleUIBlockFactory::MakeNeutral(Dict::S('UI:RunQuery:ExpressionToEvaluate'), 2));
$oQueryTextArea = new TextArea('expression', utils::EscapeHtml($sExpression), 'expression', 120, 8);
$oQueryTextArea->AddCSSClass('ibo-queryoql');
$oQueryForm->AddSubBlock($oQueryTextArea);
@@ -186,15 +188,14 @@ EOF
if (count($aArgs) > 0) {
//--- Query arguments
$oQueryArgsContainer = PanelUIBlockFactory::MakeForInformation('Query arguments')
->SetCSSClasses(['wizContainer']);
$oQueryForm->AddSubBlock(TitleUIBlockFactory::MakeNeutral(Dict::S('UI:RunQuery:QueryArguments'),2)->AddCSSClass("ibo-collapsible-section--title"));
$oQueryArgsContainer = UIContentBlockUIBlockFactory::MakeStandard(null,['wizContainer']);
$oQueryForm->AddSubBlock($oQueryArgsContainer);
foreach ($aArgs as $sParam => $sValue) {
$oArgInput = InputUIBlockFactory::MakeForInputWithLabel(
$sParam,
'arg_'.$sParam,
$sValue
);
$oInput = InputUIBlockFactory::MakeStandard("text",'arg_'.$sParam, $sValue);
$oArgInput = \Combodo\iTop\Application\UI\Base\Component\Field\FieldUIBlockFactory::MakeFromObject($sParam,$oInput,Field::ENUM_FIELD_LAYOUT_SMALL);
$oArgInput->AddCSSClass("ibo-field--label-small");
//$oArgInput = InputUIBlockFactory::MakeForInputWithLabel( $sParam, 'arg_'.$sParam, $sValue );
$oQueryArgsContainer->AddSubBlock($oArgInput);
}
}
@@ -214,10 +215,10 @@ EOF
if ($oFilter) {
//--- Query filter
$oP->AddSubBlock(TitleUIBlockFactory::MakeNeutral(Dict::S('UI:RunQuery:QueryResults'), 2));
$oPanelResult= PanelUIBlockFactory::MakeWithBrandingSecondaryColor(Dict::S('UI:RunQuery:QueryResults'));
$oP->AddSubBlock($oPanelResult);
$oResultBlock = new DisplayBlock($oFilter, 'list', false);
$oP->AddSubBlock($oResultBlock->GetDisplay($oP, 'runquery'));
$oPanelResult->AddSubBlock($oResultBlock->GetDisplay($oP, 'runquery'));
// Breadcrumb
//$iCount = $oResultBlock->GetDisplayedCount();

View File

@@ -172,33 +172,42 @@ class DBBackup
*/
public function CreateCompressedBackup($sTargetFile, $sSourceConfigFile = null)
{
$bIsCmdbSourceInitialized = CMDBSource::GetMysqli() instanceof mysqli;
if (!$bIsCmdbSourceInitialized)
{
$sErrorMsg = 'Cannot backup : CMDBSource not initialized !';
$this->LogError($sErrorMsg);
throw new CoreException($sErrorMsg);
$bReadonlyBefore = SetupUtils::EnterReadOnlyMode(MetaModel::GetConfig());
try {
//safe zone for db backup => cron is stopped/ itop in readonly
$bIsCmdbSourceInitialized = CMDBSource::GetMysqli() instanceof mysqli;
if (!$bIsCmdbSourceInitialized) {
$sErrorMsg = 'Cannot backup : CMDBSource not initialized !';
$this->LogError($sErrorMsg);
throw new CoreException($sErrorMsg);
}
$this->LogInfo("Creating backup: '$sTargetFile.tar.gz'");
$oArchive = new ITopArchiveTar($sTargetFile.'.tar.gz');
$sTmpFolder = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
$aFiles = $this->PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder);
$sFilesList = var_export($aFiles, true);
$this->LogInfo("backup: adding to archive files '$sFilesList'");
$bArchiveCreationResult = $oArchive->createModify($aFiles, '', $sTmpFolder);
if (!$bArchiveCreationResult) {
$sErrorMsg = 'Cannot backup : unable to create archive';
$this->LogError($sErrorMsg);
throw new BackupException($sErrorMsg);
}
$this->LogInfo("backup: removing tmp folder '$sTmpFolder'");
SetupUtils::rrmdir($sTmpFolder);
} finally {
if (! $bReadonlyBefore) {
SetupUtils::ExitReadOnlyMode();
} else {
//we are in the scope of main process that needs to handle/keep readonly mode (setup for example).
$this->LogInfo("Keep readonly mode after backup");
}
}
$this->LogInfo("Creating backup: '$sTargetFile.tar.gz'");
$oArchive = new ITopArchiveTar($sTargetFile.'.tar.gz');
$sTmpFolder = APPROOT.'data/tmp-backup-'.rand(10000, getrandmax());
$aFiles = $this->PrepareFilesToBackup($sSourceConfigFile, $sTmpFolder);
$sFilesList = var_export($aFiles, true);
$this->LogInfo("backup: adding to archive files '$sFilesList'");
$bArchiveCreationResult = $oArchive->createModify($aFiles, '', $sTmpFolder);
if (!$bArchiveCreationResult)
{
$sErrorMsg = 'Cannot backup : unable to create archive';
$this->LogError($sErrorMsg);
throw new BackupException($sErrorMsg);
}
$this->LogInfo("backup: removing tmp folder '$sTmpFolder'");
SetupUtils::rrmdir($sTmpFolder);
}
/**

View File

@@ -2581,6 +2581,10 @@ class MFDocument extends \Combodo\iTop\DesignDocument
public function GetNodes($sXPath, $oContextNode = null, $bSafe = true)
{
$oXPath = new DOMXPath($this);
// For Designer audit
$oXPath->registerNamespace("php", "http://php.net/xpath");
$oXPath->registerPhpFunctions();
if ($bSafe)
{
$sXPath .= "[not(@_alteration) or @_alteration!='removed']";

View File

@@ -1966,11 +1966,13 @@ JS
return APPROOT.'log/setup-queries-'.strftime('%Y-%m-%d_%H_%M').'.sql';
}
public static function EnterMaintenanceMode($oConfig)
public static function EnterMaintenanceMode($oConfig): bool
{
$bPreviousMode = self::IsInMaintenanceMode();
@touch(MAINTENANCE_MODE_FILE);
self::Log("----> Entering maintenance mode");
self::WaitCronTermination($oConfig, "maintenance");
return $bPreviousMode;
}
public static function ExitMaintenanceMode($bLog = true)
@@ -1987,11 +1989,14 @@ JS
return file_exists(MAINTENANCE_MODE_FILE);
}
public static function EnterReadOnlyMode($oConfig)
public static function EnterReadOnlyMode($oConfig): bool
{
$bPreviousMode = self::IsInReadOnlyMode();
@touch(READONLY_MODE_FILE);
self::Log("----> Entering read only mode");
self::WaitCronTermination($oConfig, "read only");
return $bPreviousMode;
}
public static function ExitReadOnlyMode($bLog = true)
@@ -2017,8 +2022,7 @@ JS
try
{
// Wait for cron to stop
if (is_null($oConfig))
{
if (is_null($oConfig) || ContextTag::Check(ContextTag::TAG_CRON)) {
return;
}
// Use mutex to check if cron is running

View File

@@ -720,7 +720,7 @@ CSS
foreach ($aLicenses as $oLicense)
{
$oPage->add('<li><b>'.$oLicense->product.'</b>, &copy; '.$oLicense->author.' is licensed under the <b>'.$oLicense->license_type.' license</b>. (<span class="toggle" id="toggle_'.$index.'">Details</span>)');
$oPage->add('<div id="license_'.$index.'" class="license_text" style="display:none;overflow:auto;max-height:10em;font-size:12px;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;"><pre>'.$oLicense->text.'</pre></div>');
$oPage->add('<div id="license_'.$index.'" class="license_text ibo-is-html-content" style="display:none;overflow:auto;max-height:10em;font-size:12px;border:1px #696969 solid;margin-bottom:1em; margin-top:0.5em;padding:0.5em;"><pre>'.$oLicense->text.'</pre></div>');
$oPage->add_ready_script('$(".license_text a").attr("target", "_blank").addClass("no-arrow");');
$oPage->add_ready_script('$("#toggle_'.$index.'").on("click", function() { $("#license_'.$index.'").toggle(); } );');
$index++;

View File

@@ -62,7 +62,7 @@ class AjaxRenderController
$oSet->SetShowObsoleteData($bShowObsoleteData);
$aResult["draw"] = $iDrawNumber;
$aResult["recordsTotal"] = $oSet->Count();
$aResult["recordsFiltered"] = $oSet->Count();
$aResult["recordsFiltered"] = $aResult["recordsTotal"] ;
$aResult["data"] = [];
while ($aObject = $oSet->FetchAssoc()) {
$aObj = [];
@@ -79,7 +79,7 @@ class AjaxRenderController
}
}
}
if (isset($aObj)) {
if (!empty($aObj)) {
if ($sIdName != "") {
if (isset($aObj[$sIdName])) {
$aObj["id"] = $aObj[$sIdName];

View File

@@ -116,6 +116,14 @@ class ActivityPanelController
'html_rendering' => $sEntryAsHtml,
];
}
// Invoke extensions after the update of the object from the activity form
/** @var \iApplicationUIExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance)
{
$oExtensionInstance->OnFormSubmit($oObject);
}
$oObject->DBWrite();
// Finalize inline images

View File

@@ -379,10 +379,17 @@ abstract class Controller
}
$aParams = array_merge($this->GetDefaultParameters(), $aParams);
$this->CreatePage($sPageType);
$this->AddToPage($this->RenderTemplate($aParams, $sTemplateName, 'html'));
$this->AddScriptToPage($this->RenderTemplate($aParams, $sTemplateName, 'js'));
$this->AddReadyScriptToPage($this->RenderTemplate($aParams, $sTemplateName, 'ready.js'));
$this->AddStyleToPage($this->RenderTemplate($aParams, $sTemplateName, 'css'));
$sHTMLContent = $this->RenderTemplate($aParams, $sTemplateName, 'html');
$this->AddToPage($sHTMLContent);
$sJSScript = $this->RenderTemplate($aParams, $sTemplateName, 'js');
$this->AddScriptToPage($sJSScript);
$sReadyScript = $this->RenderTemplate($aParams, $sTemplateName, 'ready.js');
$this->AddReadyScriptToPage($sReadyScript);
$sStyle = $this->RenderTemplate($aParams, $sTemplateName, 'css');
$this->AddStyleToPage($sStyle);
if (strlen($sHTMLContent) == 0 && strlen($sJSScript) == 0 && strlen($sReadyScript) == 0 && strlen($sStyle) == 0) {
IssueLog::Error("Missing TWIG template for $sTemplateName");
}
if (!empty($this->m_aAjaxTabs)) {
$this->m_oPage->AddTabContainer('TwigBaseTabContainer');
$this->m_oPage->SetCurrentTabContainer('TwigBaseTabContainer');

View File

@@ -228,6 +228,10 @@ class Alert extends UIContentBlock
*/
public function IsCollapsible(): bool
{
if (empty($this->sTitle)) {
return false;
}
return $this->bIsCollapsible;
}

View File

@@ -44,7 +44,10 @@ class DataTable extends UIContentBlock
protected $aAjaxData;
protected $aDisplayColumns;
protected $aResultColumns;
protected $sJsonData;
/*
* array of data to display the first page
*/
protected $aInitDisplayData;
/**
* Panel constructor.
@@ -101,6 +104,7 @@ class DataTable extends UIContentBlock
}
/**
* Get $aAjaxData as a JSON
* @return mixed
*/
public function GetJsonAjaxData(): string
@@ -179,11 +183,21 @@ class DataTable extends UIContentBlock
}
/**
* Get $aInitDisplayData as a JSON This is data of first page
* @return string
*/
public function GetJsonData(): string
public function GetJsonInitDisplayData(): string
{
return $this->sJsonData;
return json_encode($this->aInitDisplayData);
}
/**
* Get $aInitDisplayData
* @return array
*/
public function GetInitDisplayData(): array
{
return $this->aInitDisplayData;
}
/**
@@ -191,9 +205,9 @@ class DataTable extends UIContentBlock
*
* @return $this
*/
public function SetJsonData(string $sJSON)
public function SetInitDisplayData(array $aData)
{
$this->sJsonData = $sJSON;
$this->aInitDisplayData = $aData;
return $this;
}

View File

@@ -131,7 +131,11 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
}
if (!isset($aExtraParams['surround_with_panel']) || $aExtraParams['surround_with_panel']) {
$iCount = $oSet->Count();
if(!empty($oDataTable->GetInitDisplayData()) && isset($oDataTable->GetInitDisplayData()['recordsTotal'])){
$iCount = $oDataTable->GetInitDisplayData()['recordsTotal'];
} else {
$iCount = $oSet->Count();
}
$sTitle = (isset($aExtraParams['panel_title'])) ? $aExtraParams['panel_title'] : "";
$oContainer = PanelUIBlockFactory::MakeForClass($oSet->GetClass(), $sTitle)->AddCSSClass('ibo-datatable-panel');
$oContainer->SetSubTitle(Dict::Format("UI:Pagination:HeaderNoSelection", $iCount));
@@ -438,7 +442,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
]);
$oDataTable->SetDisplayColumns($aColumnDefinition);
$oDataTable->SetResultColumns($oCustomSettings->aColumns);
$oDataTable->SetJsonData(json_encode(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams)));
$oDataTable->SetInitDisplayData(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams));
return $oDataTable;
}
@@ -670,7 +674,7 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
]);
$oDataTable->SetDisplayColumns($aColumnDefinition);
$oDataTable->SetResultColumns($oCustomSettings->aColumns);
$oDataTable->SetJsonData(json_encode(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams)));
$oDataTable->SetInitDisplayData(AjaxRenderController::GetDataForTable($oSet, $aClassAliases, $aColumnsToLoad, $sIdName, $aExtraParams));
return $oDataTable;
}
@@ -713,10 +717,10 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
$aColumnDefinition["data"] = "";
$aColumnDefinition["render"]["display"] = "";
if ($sSelectMode != "single") {
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"] . " var oCheckboxElem = $('<span class=\"row_input\"><input type=\"checkbox\" class=\"selectList".$sTableId."\" name=\"selectObject\" /></span>');";
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"] . " var oCheckboxElem = $('<span class=\"row_input\"><input type=\"checkbox\" class=\"selectList".$sTableId."\" name=\"selectObject[]\" value='+row.id+' /></span>');";
}
else {
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"] . " var oCheckboxElem = $('<span class=\"row_input\"><input type=\"radio\" class=\"selectList".$sTableId."\" name=\"selectObject\" /></span>');";
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"] . " var oCheckboxElem = $('<span class=\"row_input\"><input type=\"radio\" class=\"selectList".$sTableId."\" name=\"selectObject[]\" value='+ row.id +' /></span>');";
}
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"] . " if (row.limited_access) { oCheckboxElem.html('-'); } else { oCheckboxElem.find(':input').attr('data-object-id', row.id).attr('data-target-object-id', row.target_id); }";
$aColumnDefinition["render"]["display"] = $aColumnDefinition["render"]["display"]. " return oCheckboxElem.prop('outerHTML'); ";
@@ -743,11 +747,6 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory
'attribute_label' => $aData['alias'],
];
$aColumnDefinition["data"] = $sClassAlias."/".$sAttCode;
if (MetaModel::IsValidAttCode($sClassName, 'obsolescence_flag')) {
$sDisplay = "let displayField = '<span class=\"object-ref\" title=\"".$sClassAlias."::'+data+'\"><a class=\'object-ref-link\' href=\'".$oAppRoot."/pages/UI.php?operation=details&class=".$sClassName."&id='+data+'\'>'+row['".$sClassAlias."/friendlyname']+'</a></span>'; if (row['".$sClassAlias."/obsolescence_flag'].indexOf('no') == -1){displayField = '<span class=\"object-ref obsolete\" title=\"obsolete\"><a class=\'object-ref-link\' href=\'UI.php?operation=details&class=".$sClassName."&id='+data+'\'><span class=\"object-ref-icon text_decoration\"><span class=\"fas fa-eye-slash object-obsolete fa-1x fa-fw\"></span></span>'+row['".$sClassAlias."/friendlyname']+'</a></span>';} return displayField;";
} else {
$sDisplay = "let displayField = '<span class=\"object-ref\" title=\"".$sClassAlias."::'+data+'\"><a class=\'object-ref-link\' href=\'".$oAppRoot."/pages/UI.php?operation=details&class=".$sClassName."&id='+data+'\'>'+row['".$sClassAlias."/friendlyname']+'</a></span>'; return displayField;";
}
$aColumnDefinition["render"] = [
"display" => "return row['".$sClassAlias."/hyperlink'];",
"_" => $sClassAlias."/".$sAttCode,

View File

@@ -47,7 +47,7 @@ class Panel extends UIContentBlock
/** @inheritDoc */
public const REQUIRES_ANCESTORS_DEFAULT_CSS_FILES = true;
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/panel/layout';
public const DEFAULT_JS_TEMPLATE_REL_PATH = 'base/components/panel/layout';
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/panel/layout';
public const DEFAULT_JS_FILES_REL_PATH = [
'js/components/panel.js',
];

View File

@@ -78,12 +78,42 @@ class ActivityPanelFactory
$oActivityPanel->SetCaseLogTabEntryForm($sCaseLogAttCode, CaseLogEntryFormFactory::MakeForCaselogTab($oObject, $sCaseLogAttCode, $sMode));
}
// Retrieve case logs entries
// Retrieve CMDBChange of the log entries to find their origins
//
// IMPORTANT: We don't have an easy way to find a log entry's origin, so we have to cross it with the corresponding CMDBChange
// The following code is an attempt to do so, but it might not be bulletproof against missing CMDBChanges or future code evolutions
$oCaseLogEntriesOriginsSearch = DBObjectSearch::FromOQL('SELECT C, CO FROM CMDBChange AS C JOIN CMDBChangeOpSetAttributeCaseLog AS CO ON CO.change = C.id WHERE CO.objclass = :objclass AND CO.objkey = :objkey AND CO.attcode = :attcode');
$oCaseLogEntriesOriginsSearch->SetInternalParams([
'objclass' => $sObjClass,
'objkey' => $sObjId,
'attcode' => $sCaseLogAttCode,
]);
// - Results must be in the same order as the entries in the ormCaseLog (otherwise we would need to loop over the set to find the matching index, which would not be efficient)
$oCaseLogEntriesOriginsSet = new DBObjectSet($oCaseLogEntriesOriginsSearch, ['date' => false]);
$oCaseLogEntriesOriginsSet->OptimizeColumnLoad(['C' => ['origin'], 'CO' => ['lastentry']]);
// Retrieve log entries
/** @var \ormCaseLog $oCaseLog */
$oCaseLog = $oObject->Get($sCaseLogAttCode);
// Debug message to help understand why there could be anomalies on the log entries origins
if ($oCaseLog->GetEntryCount() !== $oCaseLogEntriesOriginsSet->Count()) {
IssueLog::Debug(static::class.": Number of log entries ({$oCaseLog->GetEntryCount()}) don't match number of corresponding CMDBChanges ({$oCaseLogEntriesOriginsSet->Count()}) for object {$sObjClass}::{$sObjId} / attribute {$sCaseLogAttCode}");
}
$iCurrentEntryIndex = $oCaseLog->GetLatestEntryIndex();
foreach ($oCaseLog->GetAsArray() as $aOrmEntry) {
$oCaseLogEntry = ActivityEntryFactory::MakeFromCaseLogEntryArray($sCaseLogAttCode, $aOrmEntry);
// Try to set the origin, only if we have the corresponding data
$aChangeRow = $oCaseLogEntriesOriginsSet->FetchAssoc();
if (($aChangeRow !== null) && ($aChangeRow['CO']->Get('lastentry') === $iCurrentEntryIndex)) {
$oCaseLogEntry->SetOrigin($aChangeRow['C']->Get('origin'));
}
$oActivityPanel->AddEntry($oCaseLogEntry);
$iCurrentEntryIndex--;
}
}

View File

@@ -60,8 +60,6 @@ class UnauthenticatedWebPage extends NiceWebPage
// TODO 3.0 Find a clever way to allow theme customization for unauthenticated webpages
private $sCustomThemeUrl;
/** @var array $aReadyScripts */
protected $aReadyScripts;
/**
* @inheritDoc
@@ -71,7 +69,6 @@ class UnauthenticatedWebPage extends NiceWebPage
{
parent::__construct($s_title, $bPrintable);
$this->aReadyScripts = array();
$this->sContent = '';
$this->sPanelTitle = '';
$this->sPanelIcon = utils::GetAbsoluteUrlAppRoot().'images/itop-logo.png';
@@ -109,14 +106,6 @@ class UnauthenticatedWebPage extends NiceWebPage
$this->sContent .= $sHtml;
}
/**
* @inheritdoc
*/
public function add_ready_script($sScript)
{
$this->aReadyScripts[] = $sScript;
}
/**
* @inheritdoc
*/

View File

@@ -1 +1,11 @@
<div id="{{ oUIBlock.GetId() }}" class="ibo-breadcrumbs"></div>
<div id="{{ oUIBlock.GetId() }}" class="ibo-breadcrumbs">
<span class="ibo-breadcrumbs--previous-items-container ibo-is-hidden" data-role="ibo-breadcrumbs--previous-items-container">
<button class="ibo-breadcrumbs--previous-items-list-toggler ibo-button ibo-is-alternative ibo-is-neutral" data-role="ibo-breadcrumbs--previous-items-list-toggler"
aria-label="{{ 'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label'|dict_s }}"
data-tooltip-content="{{ 'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label'|dict_s }}"
>
<span class="fas fa-ellipsis-h"></span>
</button>
<div class="ibo-breadcrumbs--previous-items-list ibo-is-hidden" data-role="ibo-breadcrumbs--previous-items-list"></div>
</span>
</div>

View File

@@ -342,7 +342,7 @@ var aOptions{{ sListIDForVarSuffix }} = {
sSelectMode: "{{ oUIBlock.GetOption("select_mode") }}",
sSelectedItemsName: "oSelectedItems{{ sListId }}",
sViewLink: '{{ oUIBlock.GetOption("bViewLink") }}',
iPageSize: '{{ oUIBlock.GetOption("iPageSize") }}',
iPageSize: ' {{ iPageSize }}',
oClassAliases: JSON.parse('{{ oUIBlock.GetOption("oClassAliases") |raw }}'),
sTableId: '{{ oUIBlock.GetOption("sTableId") }}',
sRenderUrl: "{{ oUIBlock.GetAjaxUrl() }}",
@@ -351,7 +351,8 @@ var aOptions{{ sListIDForVarSuffix }} = {
oLabels: {moveup: "{{ 'UI:Button:MoveUp'|dict_s }}", movedown: "{{ 'UI:Button:MoveDown'|dict_s }}"},
};
if ($('#datatable_dlg_{{ oUIBlock.GetId() }}').hasClass('itop-datatable')) {
if ($('#datatable_dlg_{{ oUIBlock.GetId() }}').hasClass('itop-datatable'))
{
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings('destroy');
}
$('#datatable_dlg_{{ oUIBlock.GetId() }}').DataTableSettings(aOptions{{ sListIDForVarSuffix }});

View File

@@ -24,6 +24,8 @@
<div class="ibo-caselog-entry-form--text-input" data-role="ibo-caselog-entry-form--text-input">
{{ render_block(oUIBlock.GetTextInput(), {aPage: aPage}) }}
</div>
<div class="ibo-caselog-entry-form--extra-inputs" data-role="ibo-caselog-entry-form--extra-inputs">
</div>
<div class="ibo-caselog-entry-form--lock-indicator ibo-is-hidden" data-role="ibo-caselog-entry-form--lock-indicator">
<span class="ibo-caselog-entry-form--lock-icon" data-role="ibo-caselog-entry-form--lock-icon">
<span class="fas fa-lock"></span>

View File

@@ -31,7 +31,7 @@
{% endif %}
{% endblock %}
{% endfor %}
<li class="ibo-tab-container--extra-tabs-container ibo-tab-container--tab-header" data-role="ibo-tab-container--extra-tabs-container">
<li class="ibo-tab-container--extra-tabs-container ibo-tab-container--tab-header ibo-is-hidden" data-role="ibo-tab-container--extra-tabs-container">
<a href="#" class="ibo-tab-container--extra-tabs-list-toggler" data-role="ibo-tab-container--extra-tabs-list-toggler"
aria-label="{{ 'UI:Layout:TabContainer:ExtraTabsListToggler:Label'|dict_s }}"
data-tooltip-content="{{ 'UI:Layout:TabContainer:ExtraTabsListToggler:Label'|dict_s }}"

View File

@@ -15,8 +15,8 @@ class iTopConfigAstValidatorTest extends ItopTestCase
protected function setUp()
{
parent::setUp();
require_once APPROOT.'datamodels/2.x/itop-config/src/Validator/iTopConfigAstValidator.php';
require_once APPROOT.'datamodels/2.x/itop-config/src/Validator/ConfigNodesVisitor.php';
require_once APPROOT.'env-production/itop-config/src/Validator/iTopConfigAstValidator.php';
require_once APPROOT.'env-production/itop-config/src/Validator/ConfigNodesVisitor.php';
}
public function testValidateFileValid()

View File

@@ -0,0 +1,40 @@
<?php
/*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ormCaseLog;
/**
* Tests of the ormCaseLog class
*
* @covers \ormCaseLog
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class ormCaseLogTest extends ItopDataTestCase
{
/**
* @covers \ormCaseLog::GetEntryCount()
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \OQLException
*/
public function testGetEntryCount()
{
// New log, with no entry
$oLog = new ormCaseLog();
$this->assertEquals($oLog->GetEntryCount(), 0, 'Should be no entry yet, returned '.$oLog->GetEntryCount());
// Add an entry
$oLog->AddLogEntry('First entry');
$this->assertEquals($oLog->GetEntryCount(), 1, 'Should be 1 entry, returned '.$oLog->GetEntryCount());
}
}