Compare commits

...

7 Commits

Author SHA1 Message Date
Timmy38
3e38e349e5 N°9690 Fix code style 2026-06-15 11:53:14 +02:00
Timmy38
65b58ec4e2 N°9690 Removed and added modules/extensions are now read from GET parameters 2026-06-15 11:49:52 +02:00
Timmy38
6a20e36434 N°9683 Fix Code Style 2026-06-12 11:23:35 +02:00
Vincent Dumas
3805a322c1 N°9160 Copy log entry from parent ticket (#927)
* N°9160 - Refactor UpdateChildxxxxxLog and add test
2026-06-12 09:09:02 +02:00
odain
0bf773a23c N°9454 - fix unattended regression when fresh install and no conf yet 2026-06-11 18:47:48 +02:00
Timmy38
28a09068a0 N°9683 Add markdown in UIBlock for behat 2026-06-11 17:14:48 +02:00
lenaick.moreira
db7fd6b3c0 N°9638 - Rollback undesired changes 2026-06-11 09:41:45 +02:00
33 changed files with 353 additions and 180 deletions

View File

@@ -460,6 +460,18 @@
<extkey_attcode>parent_incident_id</extkey_attcode>
<target_attcode>ref</target_attcode>
</field>
<field id="parent_request_id" xsi:type="AttributeExternalKey">
<filter><![CDATA[SELECT UserRequest WHERE id != :this->id AND status NOT IN ('rejected','resolved','closed')]]></filter>
<dependencies/>
<sql>parent_request_id</sql>
<target_class>UserRequest</target_class>
<is_null_allowed>true</is_null_allowed>
<on_target_delete>DEL_MANUAL</on_target_delete>
</field>
<field id="parent_request_ref" xsi:type="AttributeExternalField">
<extkey_attcode>parent_request_id</extkey_attcode>
<target_attcode>ref</target_attcode>
</field>
<field id="parent_problem_id" xsi:type="AttributeExternalKey">
<sql>parent_problem_id</sql>
<target_class>Problem</target_class>
@@ -987,6 +999,9 @@
<attribute id="parent_incident_id">
<read_only/>
</attribute>
<attribute id="parent_request_id">
<read_only/>
</attribute>
<attribute id="parent_change_id">
<read_only/>
</attribute>
@@ -1301,90 +1316,20 @@
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
if (!MetaModel::IsValidClass('UserRequest')) return true; // Do nothing
$oLog = $this->Get('public_log');
$sLogPublic = $oLog->GetModifiedEntry('html');
if ($sLogPublic != '')
{
$sOQL = "SELECT UserRequest WHERE parent_incident_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('public_log',$sLogPublic);
$oRequest->DBUpdate();
}
}
$oLog = $this->Get('private_log');
$sLogPrivate = $oLog->GetModifiedEntry('html');
if ($sLogPrivate != '')
{
$sOQL = "SELECT UserRequest WHERE parent_incident_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('private_log',$sLogPrivate);
$oRequest->DBUpdate();
}
}
return true;
if (MetaModel::IsValidClass('UserRequest')) {
return $this->UpdateChildTicketLog('UserRequest', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
}
return true;
}]]></code>
</method>
<method id="UpdateChildIncidentLog">
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildIncidentLog()
{
$oLog = $this->Get('public_log');
$sLogPublic = $oLog->GetModifiedEntry('html');
if ($sLogPublic != '')
{
$sOQL = "SELECT Incident WHERE parent_incident_id=:ticket";
$oChildIncidentSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oIncident = $oChildIncidentSet->Fetch())
{
$oIncident->set('public_log',$sLogPublic);
$oIncident->DBUpdate();
}
}
$oLog = $this->Get('private_log');
$sLogPrivate = $oLog->GetModifiedEntry('html');
if ($sLogPrivate != '')
{
$sOQL = "SELECT Incident WHERE parent_incident_id=:ticket";
$oChildIncidentSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oIncident = $oChildIncidentSet->Fetch())
{
$oIncident->set('private_log',$sLogPrivate);
$oIncident->DBUpdate();
}
}
return true;
return $this->UpdateChildTicketLog('Incident', 'parent_incident_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
}]]></code>
</method>
<method id="ComputeImpactedItems">
@@ -1558,6 +1503,9 @@
<item id="parent_incident_id">
<rank>10</rank>
</item>
<item id="parent_request_id">
<rank>15</rank>
</item>
<item id="parent_problem_id">
<rank>20</rank>
</item>

View File

@@ -44,6 +44,8 @@ Dict::Add('EN US', 'English', 'English', [
'UI-IncidentManagementOverview-OpenIncidentByStatus' => 'Open incidents by status',
'UI-IncidentManagementOverview-OpenIncidentByAgent' => 'Open incidents by agent',
'UI-IncidentManagementOverview-OpenIncidentByCustomer' => 'Open incidents by customer',
'Class:Incident/Method:UpdateChildTicketWith:public_log' => '<i><u>Public log entry from parent Incident %2$s:</u></i><br><br>',
'Class:Incident/Method:UpdateChildTicketWith:private_log' => '<i>Private log entry from parent Incident [[Incident:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions
@@ -193,6 +195,10 @@ Dict::Add('EN US', 'English', 'English', [
'Class:Incident/Attribute:parent_incident_id+' => '',
'Class:Incident/Attribute:parent_incident_ref' => 'Parent incident ref',
'Class:Incident/Attribute:parent_incident_ref+' => '',
'Class:Incident/Attribute:parent_request_id' => 'Parent request',
'Class:Incident/Attribute:parent_request_id+' => '',
'Class:Incident/Attribute:parent_request_ref' => 'Parent request ref',
'Class:Incident/Attribute:parent_request_ref+' => '',
'Class:Incident/Attribute:parent_change_id' => 'Parent change',
'Class:Incident/Attribute:parent_change_id+' => '',
'Class:Incident/Attribute:parent_change_ref' => 'Parent change ref',

View File

@@ -179,15 +179,19 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:Incident/Attribute:pending_reason+' => '',
'Class:Incident/Attribute:parent_incident_id' => 'Incident parent',
'Class:Incident/Attribute:parent_incident_id+' => '',
'Class:Incident/Attribute:parent_incident_ref' => 'Référence incident parent',
'Class:Incident/Attribute:parent_incident_ref' => 'Réf. incident parent',
'Class:Incident/Attribute:parent_incident_ref+' => '',
'Class:Incident/Attribute:parent_request_id' => 'Demande parente',
'Class:Incident/Attribute:parent_request_id+' => '',
'Class:Incident/Attribute:parent_request_ref' => 'Réf. demande parente',
'Class:Incident/Attribute:parent_request_ref+' => '',
'Class:Incident/Attribute:parent_change_id' => 'Changement parent',
'Class:Incident/Attribute:parent_change_id+' => '',
'Class:Incident/Attribute:parent_change_ref' => 'Ref Changement parent',
'Class:Incident/Attribute:parent_change_ref' => 'Réf. changement parent',
'Class:Incident/Attribute:parent_change_ref+' => '',
'Class:Incident/Attribute:parent_problem_id' => 'Problème lié',
'Class:Incident/Attribute:parent_problem_id+' => '',
'Class:Incident/Attribute:parent_problem_ref' => 'Référence problème lié',
'Class:Incident/Attribute:parent_problem_ref' => 'Réf. problème lié',
'Class:Incident/Attribute:parent_problem_ref+' => '',
'Class:Incident/Attribute:related_request_list' => 'Requêtes filles',
'Class:Incident/Attribute:related_request_list+' => '',

View File

@@ -1451,44 +1451,17 @@
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
return $this->UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log' ]);
}]]></code>
</method>
<method id="UpdateChildIncidentLog">
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildIncidentLog()
{
$oLog = $this->Get('public_log');
$sLogPublic = $oLog->GetModifiedEntry('html');
if ($sLogPublic != '')
{
$sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('public_log',$sLogPublic);
$oRequest->DBUpdate();
}
}
$oLog = $this->Get('private_log');
$sLogPrivate = $oLog->GetModifiedEntry('html');
if ($sLogPrivate != '')
{
$sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('private_log',$sLogPrivate);
$oRequest->DBUpdate();
}
}
return true;
return $this->UpdateChildTicketLog('Incident', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log']);
}]]></code>
</method>
<method id="ComputeImpactedItems">
@@ -1526,6 +1499,7 @@
parent::OnUpdate();
$this->Set('last_update', time());
$this->UpdateChildRequestLog();
$this->UpdateChildIncidentLog();
}]]></code>
</method>
</methods>

View File

@@ -37,6 +37,8 @@ Dict::Add('EN US', 'English', 'English', [
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Open requests by customer',
'Class:UserRequest:KnownErrorList' => 'Known Errors',
'Class:UserRequest:KnownErrorList+' => 'Known Errors related to Functional CI linked to the current ticket',
'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => '<i><u>Public log automatic copy from parent User Request %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Private log automatic copy from parent User Request [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -42,6 +42,8 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Requêtes ouvertes par client',
'Class:UserRequest:KnownErrorList' => 'Erreurs connues',
'Class:UserRequest:KnownErrorList+' => 'Erreurs connues liées à des éléments de configuration impactés par ce ticket',
'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => '<i><u>Copie automatique du log public de la demande parente %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -1486,44 +1486,8 @@
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildRequestLog()
{
$oLog = $this->Get('public_log');
$sLogPublic = $oLog->GetModifiedEntry('html');
if ($sLogPublic != '')
{
$sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('public_log',$sLogPublic);
$oRequest->DBUpdate();
}
}
$oLog = $this->Get('private_log');
$sLogPrivate = $oLog->GetModifiedEntry('html');
if ($sLogPrivate != '')
{
$sOQL = "SELECT UserRequest WHERE parent_request_id=:ticket";
$oChildRequestSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL),
array(),
array(
'ticket' => $this->GetKey(),
)
);
while($oRequest = $oChildRequestSet->Fetch())
{
$oRequest->set('private_log',$sLogPrivate);
$oRequest->DBUpdate();
}
}
return true;
{
return $this->UpdateChildTicketLog('UserRequest', 'parent_request_id', ['public_log' => 'public_log', 'private_log' => 'private_log'] );
}]]></code>
</method>
<method id="ComputeImpactedItems">

View File

@@ -41,6 +41,8 @@ Dict::Add('EN US', 'English', 'English', [
'Menu:UserRequest:MyWorkOrders+' => 'All work orders assigned to me',
'Class:Problem:KnownProblemList' => 'Known problems',
'Tickets:Related:OpenIncidents' => 'Open incidents',
'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => '<i><u>Public log automatic copy from parent User Request %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Private log automatic copy from parent User Request [[UserRequest:%1$s]]:</i><br><br>',
]);
// Dictionnay conventions

View File

@@ -42,6 +42,8 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI-RequestManagementOverview-OpenRequestByCustomer' => 'Requêtes ouvertes par organisation',
'Class:UserRequest:KnownErrorList' => 'Erreurs connues',
'Class:UserRequest:KnownErrorList+' => 'Erreurs connues liées à des éléments de configuration impactés par ce ticket',
'Class:UserRequest/Method:UpdateChildTicketWith:public_log' => '<i><u>Copie automatique du log public de la demande parente %2$s:</u></i><br><br>',
'Class:UserRequest/Method:UpdateChildTicketWith:private_log' => '<i>Copie automatique du log privé de la demande parente [[UserRequest:%1$s]]:</i><br><br>',
'Menu:UserRequest:MyWorkOrders' => 'Tâches qui me sont assignées',
'Menu:UserRequest:MyWorkOrders+' => '',
'Class:Problem:KnownProblemList' => 'Problèmes connus',

View File

@@ -351,6 +351,63 @@
}]]></code>
<arguments/>
</method>
<method id="UpdateChildTicketLog">
<comment><![CDATA[/**
*
* Remove the current user associated Person from the contacts_list of this Ticket
* No error if there is no associated Person or if the Person is not in the list
* @return bool Return true
* @param string $sChildClass The class name of the child ticket (e.g. 'Incident', 'UserRequest', etc.)
* @param string $sChildParentAttCode The external key in the child class pointing to the parent ticket (e.g. 'parent_request_id')
* @param array $aLogAttCodes An array of parent caselog attribute codes and for each the corresponding child log attribute code to update (e.g. ['public_log' => 'private_log'])
* So in the example parent.public_log will be copied into each child.private_log
* with a prefix using a dictionary entry like 'Class:<parent_ticket_class>/Method:UpdateChildTicketWith:<caselog_attcode>' with one placeholder %1$s for the parent ticket ref
* resulting in an entry like "Copy of public log entry from parent Incident I-000123: <log entry from parent.public_log>" in the child private_log
*
*/]]>
</comment>
<static>false</static>
<access>public</access>
<type>LifecycleAction</type>
<code><![CDATA[ public function UpdateChildTicketLog($sChildClass, $sChildParentAttCode, $aLogAttCodes)
{
if (!MetaModel::IsValidClass($sChildClass) || (!MetaModel::IsValidAttCode($sChildClass, $sChildParentAttCode))) {
ErrorLog::Debug("Invalid class ($sChildClass) or attribute code ($sChildParentAttCode) provided to UpdateChildTicketLog","DataModel");
return true; // Do nothing
}
$oAttDef = MetaModel::GetAttributeDef($sChildClass, $sChildParentAttCode);
if (!$oAttDef instanceof AttributeExternalKey || ($oAttDef->GetTargetClass() !== get_class($this))) {
ErrorLog::Debug("Attribute $sChildParentAttCode should be an external key of class $sChildClass, pointing to class ".get_class($this),"DataModel");
return true; // Do nothing
}
$sParentClass = get_class($this);
$aChildEntries = [];
foreach ($aLogAttCodes as $sParentAttCode => $sChildAttCode) {
if (MetaModel::IsValidAttCode($sParentClass, $sParentAttCode) && MetaModel::GetAttributeDef($sParentClass, $sParentAttCode) instanceof AttributeCaseLog
&& MetaModel::IsValidAttCode($sChildClass, $sChildAttCode) && MetaModel::GetAttributeDef($sChildClass, $sChildAttCode) instanceof AttributeCaseLog
&& (!utils::IsNullOrEmptyString($this->Get($sParentAttCode)->GetModifiedEntry('html')))) {
$aChildEntries[$sChildAttCode] = Dict::Format('Class:'.$sParentClass.'/Method:UpdateChildTicketWith:'.$sParentAttCode, $this->GetKey(), $this->Get('ref')).$this->Get($sParentAttCode)->GetModifiedEntry('html');
}
}
if ($aChildEntries == []) {
return true; // nothing to update
}
$sOQL = "SELECT $sChildClass WHERE $sChildParentAttCode = :ticket_id";
$oChildSet = new DBObjectSet(DBObjectSearch::FromOQL_AllData($sOQL), [], ['ticket_id' => $this->GetKey()]);
while ($oChild = $oChildSet->Fetch()) {
if (is_object($oChild)) { // Seems that empty set, maybe in case of OQL syntax error, can return a single empty object instead of no object at all
foreach ($aChildEntries as $sAttCode => $sEntry) {
$oChild->Set($sAttCode, $sEntry);
}
$oChild->DBUpdate();
}
}
return true;
}]]></code>
</method>
</methods>
<presentation>
<details>

View File

@@ -21,4 +21,5 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -19,4 +19,5 @@ Dict::Add('EN US', 'English', 'English', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'Plus d\'informations',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Forcer la désinstallation',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Plus d\'actions',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -21,4 +21,5 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'UI:Layout:ExtensionsDetails:MenuAbout' => 'More informations~~',
'UI:Layout:ExtensionsDetails:MenuForce' => 'Force uninstall~~',
'UI:Layout:ExtensionsDetails:MoreActions' => 'Show more actions~~',
'UI:Layout:ExtensionsDetails:TogglerTooltip' => 'Toggle %1$s~~',
]);

View File

@@ -258,7 +258,7 @@ class InstallationFileService
public function ProcessDefaultModules(): void
{
$sProductionModuleDir = APPROOT.'data/'.$this->sTargetEnvironment.'-modules/';
$oConfig = new Config(APPCONF.$this->sTargetEnvironment.'/'.ITOP_CONFIG_FILE);
$oConfig = new Config(APPCONF.$this->sTargetEnvironment.'/'.ITOP_CONFIG_FILE, false);
$aAvailableModules = $this->GetProductionEnv()->AnalyzeInstallation($oConfig, $this->GetExtraDirs());

View File

@@ -18,10 +18,12 @@ class Toggler extends Input
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/input/input-toggler';
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/input/input-toggler';
public function __construct(?string $sId = null)
protected string $sTooltip = '';
public function __construct(?string $sId = null, string $sTooltip = '')
{
parent::__construct($sId);
$this->SetType('checkbox');
$this->SetTooltip($sTooltip);
}
public function SetIsToggled(bool $bIsToggled): static
@@ -33,4 +35,13 @@ class Toggler extends Input
{
return $this->IsChecked();
}
public function GetTooltip(): string
{
return $this->sTooltip;
}
public function SetTooltip($sTooltip): void
{
$this->sTooltip = $sTooltip;
}
}

View File

@@ -182,13 +182,14 @@ class ExtensionDetails extends UIContentBlock
$sName = 'aSelectedExtensions['.$this->GetCode().']';
$this->oToggler = new Toggler();
$this->oToggler->SetName($sName);
$this->oToggler->SetTooltip(Dict::Format('UI:Layout:ExtensionsDetails:TogglerTooltip', $this->GetLabel()));
$this->oToggler->AddCSSClass('toggler-install');
}
protected function InitializePopoverMenu()
{
$this->oPopoverMenu = new PopoverMenu();
$oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S('UI:Layout:ExtensionsDetails:MoreActions'));
$oPopoverOpenButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S('UI:Layout:ExtensionsDetails:MoreActions'), 'dropdown-menu-'.$this->GetCode());
$this->oPopoverMenu->SetTogglerFromBlock($oPopoverOpenButton);
$this->oMoreActions = new UIContentBlock();
$this->oMoreActions->AddSubBlock($this->oPopoverMenu);

View File

@@ -1794,9 +1794,11 @@ JS;
}
/**
* @param bool $bReturnOutput
*
* @throws \Exception
*/
protected function output_dict_entries()
protected function output_dict_entries($bReturnOutput = false)
{
if ($this->sContentType != 'text/plain' && $this->sContentType != 'application/json' && $this->sContentType != 'application/javascript') {
/** @var \iBackofficeDictEntriesExtension $oExtensionInstance */

View File

@@ -2,7 +2,7 @@
{% block iboInput %}
<span class="ibo-toggler--wrapper">
{{ parent() }}
<span class="ibo-toggler--slider"></span>
<a class="ibo-toggler--slider" title="{{ oUIBlock.GetTooltip() }}" href="#"></a>
<input class="ibo-toggler--hidden" type="hidden" name="{{ oUIBlock.GetName() }}" value="{% if oUIBlock.IsChecked() %}on{% else %}off{% endif %}"/>
</span>
{% endblock %}

View File

@@ -6,12 +6,6 @@ require_once(dirname(__DIR__, 6).'/approot.inc.php');
require_once(APPROOT.'application/startup.inc.php');
require_once(APPROOT.'setup/setuputils.class.inc.php');
$aParams = [
"exec_module" => "combodo-data-feature-removal",
"exec_page" => "index.php",
'exec_env' => 'production',
];
new ContextTag(ContextTag::TAG_SETUP);
$sToken = SetupUtils::CreateSetupToken();
@@ -34,24 +28,45 @@ function GetLastestInstallFile(): ?string
return $sLastFilePath;
}
$aRemovedExtensions = [
'itop-container-mgmt' => 'Containerization',
];
$sPath = GetLastestInstallFile();
if (is_null($sPath)) {
throw new Exception("$sPath no installation XM. Launch a setup....");
throw new Exception("$sPath no installation XML. Launch a setup....");
}
$aParams = new XMLParameters($sPath);
$aSelectedModules = array_filter($aParams->Get('selected_modules', []), static function ($element) {
global $aRemovedExtensions;
return ! array_key_exists($element, $aRemovedExtensions);
});
$aSelectedModules = $aParams->Get('selected_modules', []);
$aSelectedExtensions = $aParams->Get('selected_extensions', []);
$aSelectedExtensions = array_filter($aParams->Get('selected_extensions', []), static function ($element) {
global $aRemovedExtensions;
return ! array_key_exists($element, $aRemovedExtensions);
});
$sAddedExtensions = utils::ReadParam('added_extensions', '');
$aAddedExtensions = [];
if (mb_strlen($sAddedExtensions) > 0) {
$aAddedExtensions = explode(',', $sAddedExtensions);
}
$oExtensionMap = new iTopExtensionsMap();
foreach ($aAddedExtensions as $sExtensionCode) {
if (mb_strlen($sExtensionCode) <= 0) {
continue;
}
$oExtension = $oExtensionMap->GetFromExtensionCode($sExtensionCode);
$aSelectedExtensions[] = $oExtension->sCode;
foreach ($oExtension->aModules as $sModuleCode) {
if (!in_array($sModuleCode, $aSelectedModules)) {
$aSelectedModules[] = $sModuleCode;
}
}
}
$sRemovedExtensions = utils::ReadParam('removed_modules', '', false, 'raw');
$aRemovedExtensionsAndModules = [];
if (mb_strlen($sRemovedExtensions) > 0) {
$aRemovedExtensionsAndModules = explode(',', $sRemovedExtensions);
}
$aSelectedModules = array_filter($aSelectedModules, fn ($element) => !in_array($element, $aRemovedExtensionsAndModules));
$aSelectedExtensions = array_filter($aSelectedExtensions, fn ($element) => !in_array($element, $aRemovedExtensionsAndModules));
$aRemovedExtensionsAndModules = array_filter($aRemovedExtensionsAndModules, fn ($element) => !is_null($oExtensionMap->GetFromExtensionCode($element)));
$aRemovedExtensions = array_combine($aRemovedExtensionsAndModules, $aRemovedExtensionsAndModules);
$aAddedExtensions = array_combine($aAddedExtensions, $aAddedExtensions);
$aPostParams = [
"auth_user" => 'admin',
@@ -62,8 +77,9 @@ $aPostParams = [
'selected_modules' => utils::HtmlEntities(json_encode($aSelectedModules)),
'selected_extensions' => utils::HtmlEntities(json_encode($aSelectedExtensions)),
'removed_extensions' => utils::HtmlEntities(json_encode($aRemovedExtensions)),
'force-uninstall' => "on",
'use_symbolic_links' => "",
'added_extensions' => utils::HtmlEntities(json_encode($aAddedExtensions)),
'force-uninstall' => 'on',
'use_symbolic_links' => '',
];
$sHiddenPostedInput = "";

View File

@@ -0,0 +1,166 @@
<?php
// Copyright (c) 2010-2024 Combodo SAS
//
// 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
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
namespace Combodo\iTop\Test\UnitTest\Module\iTopTickets;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ormCaseLog;
use MetaModel;
class UpdateChildTicketLogTest extends ItopDataTestCase
{
public function testUpdateChildTicketLog_PublicLogOnTwoChild(): void
{
//Given a parent ticket with two child ticket
list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(2);
$this->assertCount(2, $aChildrenTree[$iParentTicket], 'The test setup should create exactly two child tickets.');
$sParentPublicLogEntry = 'This is a public log entry for the parent ticket.';
$oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
// When I enter a public_log entry for the parent ticket
$oParentTicket->Set('public_log', $sParentPublicLogEntry);
$oParentTicket->DBUpdate();
// Then the log should be copied to all descendants and contain parent references recursively
$this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'public_log', $sParentPublicLogEntry);
}
public function testUpdateChildTicketLog_PrivateAndPublicLog(): void
{
//Given a parent ticket with two child ticket
list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(3);
$sParentPublicLogEntry = 'This is a public log entry for the parent ticket.';
$sParentPrivateLogEntry = 'This is a private log entry for the parent ticket.';
// When I enter both a public_log and a private_log entry for the parent ticket
$oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
$oParentTicket->Set('public_log', $sParentPublicLogEntry);
$oParentTicket->Set('private_log', $sParentPrivateLogEntry);
$oParentTicket->DBUpdate();
// Then both logs should be copied to all descendants and keep ancestor references recursively
$this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'public_log', $sParentPublicLogEntry);
$this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'private_log', $sParentPrivateLogEntry);
}
public function testUpdateChildTicketLog_PrivateLogOnMultipleLevels(): void
{
//Given a parent ticket with two child ticket
list($iParentTicket, $aChildrenTree) = $this->GivenUserRequests(1, 4);
$sParentPrivateLogEntry = 'This is a private log entry for the parent ticket.';
// When I enter both a public_log and a private_log entry for the parent ticket
$oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
$oParentTicket->Set('private_log', $sParentPrivateLogEntry);
$oParentTicket->DBUpdate();
// Then the private log should be copied on each level with parent + grand-parent +... references
$this->AssertLogContainsParentsRefOrKeyRecursively($iParentTicket, $aChildrenTree[$iParentTicket], 'private_log', $sParentPrivateLogEntry);
}
private function AssertLogContainsParentsRefOrKeyRecursively(int $iParentTicket, array $aChildrenTree, string $sLogAttCode, string $sExpectedEntry, array $aAncestors = []): void
{
$oParentTicket = MetaModel::GetObject('UserRequest', $iParentTicket);
$aAncestorsToFind = array_merge($aAncestors, [[
'id' => (string) $iParentTicket,
'ref' => (string) $oParentTicket->Get('ref'),
]]);
foreach ($aChildrenTree as $iChildOrIndex => $aChildNodeOrId) {
if (is_array($aChildNodeOrId)) {
$iChildTicket = (int) $iChildOrIndex;
$aGrandChildrenTree = $aChildNodeOrId;
} else {
$iChildTicket = (int) $aChildNodeOrId;
$aGrandChildrenTree = [];
}
$oChildTicket = MetaModel::GetObject('UserRequest', $iChildTicket);
$sLastLogEntry = $oChildTicket->Get($sLogAttCode)->GetLatestEntry();
$this->assertNotEmpty($sLastLogEntry, "The $sLogAttCode entry was not copied to child ticket #$iChildTicket.");
$this->assertStringContainsString($sExpectedEntry, $sLastLogEntry, "The $sLogAttCode entry on child ticket #$iChildTicket does not contain the original parent entry.");
foreach ($aAncestorsToFind as $aAncestor) {
$bContainsAncestorRef = strpos($sLastLogEntry, $aAncestor['ref']) !== false;
$bContainsAncestorId = strpos($sLastLogEntry, $aAncestor['id']) !== false;
$this->assertTrue(
$bContainsAncestorRef || $bContainsAncestorId,
"The $sLogAttCode entry on child ticket #$iChildTicket does not contain ancestor ref '{$aAncestor['ref']}' nor ancestor id '{$aAncestor['id']}'."
);
}
if ($aGrandChildrenTree !== []) {
$this->AssertLogContainsParentsRefOrKeyRecursively($iChildTicket, $aGrandChildrenTree, $sLogAttCode, $sExpectedEntry, $aAncestorsToFind);
}
}
}
/**
* @return array
* @throws \Exception
*/
private function GivenUserRequests(int $iCount, int $iLevel = 2): array
{
$iOrg = $this->GivenObjectInDB('Organization', [
'name' => 'Test Organization for Log Update',
]);
// Given a parent ticket
$iParentTicket = $this->GivenObjectInDB('UserRequest', [
'title' => 'Parent Ticket for Log Update Test',
'description' => 'This is the parent ticket for testing log updates.',
'org_id' => $iOrg,
'ref' => 'R-00001',
]);
$iRemainingLevels = max(0, $iLevel - 1);
$iRefCounter = 1;
$aChildrenTree = [
$iParentTicket => $this->GivenChildTicketsRecursive($iParentTicket, $iCount, $iRemainingLevels, $iOrg, $iRefCounter),
];
return [$iParentTicket, $aChildrenTree];
}
private function GivenChildTicketsRecursive(int $iParentTicket, int $iCount, int $iRemainingLevels, int $iOrg, int &$iRefCounter): array
{
if ($iRemainingLevels <= 0) {
return [];
}
$aChildren = [];
for ($i = 1; $i <= $iCount; $i++) {
$iRefCounter++;
$sRef = sprintf('R-%05d', $iRefCounter);
$iChildTicket = $this->GivenObjectInDB('UserRequest', [
'title' => "Child Ticket $sRef for Log Update Test",
'description' => "This is child ticket $sRef for testing log updates.",
'org_id' => $iOrg,
'parent_request_id' => $iParentTicket,
'ref' => $sRef,
]);
if ($iRemainingLevels > 1) {
$aChildren[$iChildTicket] = $this->GivenChildTicketsRecursive($iChildTicket, $iCount, $iRemainingLevels - 1, $iOrg, $iRefCounter);
} else {
$aChildren[] = $iChildTicket;
}
}
return $aChildren;
}
}