Compare commits

...

2 Commits

Author SHA1 Message Date
Anne-Cath
46222d750f N°9542 - remove the hard values 2026-04-24 17:07:17 +02:00
Anne-Cath
87977863c9 N°9542 - Portal : add the possibility to choose subclass in search screen 2026-04-24 16:22:41 +02:00
3 changed files with 205 additions and 29 deletions

View File

@@ -52,6 +52,13 @@ p_object_search_from_attribute:
sHostObjectClass: ~
sHostObjectId: ~
p_columns_from_attribute_with_class:
path: '/object/search/columns-from-attribute-with-class/{sTargetAttCode}/{sHostObjectClass}/{sHostObjectId}'
defaults:
_controller: 'Combodo\iTop\Portal\Controller\ObjectController::GetColumnsFromAttributeAction'
sHostObjectClass: ~
sHostObjectId: ~
p_object_search_autocomplete:
path: '/object/search/autocomplete/{sTargetAttCode}/{sHostObjectClass}/{sHostObjectId}'
defaults:

View File

@@ -714,6 +714,125 @@ class ObjectController extends BrickController
return $oResponse;
}
public function GetColumnsFromAttributeAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
{
$sFinalClass = $this->oRequestManipulatorHelper->ReadParam('finalclass', null, FILTER_UNSAFE_RAW);
/** @var array $aCombodoPortalInstanceConf */
$aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf');
$aData = [
'sMode' => 'search_regular',
'sTargetAttCode' => $sTargetAttCode,
'sHostObjectClass' => $sHostObjectClass,
'sHostObjectId' => $sHostObjectId,
'sActionRulesToken' => $this->oRequestManipulatorHelper->ReadParam('ar_token', ''),
];
// Checking security layers
if (!$this->oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sHostObjectClass, $sHostObjectId)) {
IssueLog::Warning(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' not allowed to read '.$sHostObjectClass.'::'.$sHostObjectId.' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
// Retrieving host object for future DBSearch parameters
if ($sHostObjectId !== null) {
// Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
$oHostObject = MetaModel::GetObject($sHostObjectClass, $sHostObjectId, true, true);
} else {
$oHostObject = MetaModel::NewObject($sHostObjectClass);
// Retrieving action rules
//
// Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values.
// But it would not be a security issue as it only presets values in the form.
$aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : [];
// Preparing object
$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oHostObject);
}
// Updating host object with form data / values
$sFormManagerClass = $this->oRequestManipulatorHelper->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
$sFormManagerData = $this->oRequestManipulatorHelper->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
if (!empty($sFormManagerClass) && !empty($sFormManagerData)) {
/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */
$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
$oFormManager->SetObjectFormHandlerHelper($this->oObjectFormHandlerHelper);
$oFormManager->SetObject($oHostObject);
// Applying action rules if present
if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) {
$aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
$oObj = $oFormManager->GetObject();
$this->oContextManipulatorHelper->PrepareObject($aActionRules, $oObj);
$oFormManager->SetObject($oObj);
}
// Updating host object
$oFormManager->OnUpdate([
'currentValues' => $this->oRequestManipulatorHelper->ReadParam('current_values', [], FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY),
]);
$oHostObject = $oFormManager->GetObject();
}
// Retrieving request parameters
$sFieldId = $this->oRequestManipulatorHelper->ReadParam('sFieldId', '');
// Building search query
// - Retrieving target object class from attcode
$oTargetAttDef = MetaModel::GetAttributeDef($sHostObjectClass, $sTargetAttCode);
if ($oTargetAttDef->IsExternalKey()) {
/** @var \AttributeExternalKey $oTargetAttDef */
$sTargetObjectClass = $oTargetAttDef->GetTargetClass();
} elseif ($oTargetAttDef->IsLinkSet()) {
/** @var \AttributeLinkedSet $oTargetAttDef */
if (!$oTargetAttDef->IsIndirect()) {
$sTargetObjectClass = $oTargetAttDef->GetLinkedClass();
} else {
/** @var \AttributeLinkedSetIndirect $oTargetAttDef */
/** @var \AttributeExternalKey $oRemoteAttDef */
$oRemoteAttDef = MetaModel::GetAttributeDef($oTargetAttDef->GetLinkedClass(), $oTargetAttDef->GetExtKeyToRemote());
$sTargetObjectClass = $oRemoteAttDef->GetTargetClass();
}
} elseif ($oTargetAttDef->GetEditClass() === 'CustomFields') {
$oRequestTemplate = $oHostObject->Get($sTargetAttCode);
/** @var \DBSearch $oTemplateFieldSearch */
$oTemplateFieldSearch = $oRequestTemplate->GetForm()->GetField('user_data')->GetForm()->GetField($sFieldId)->GetSearch();
$sTargetObjectClass = $oTemplateFieldSearch->GetClass();
} else {
throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, '.get_class($oTargetAttDef).' given.');
}
if (!empty($sFinalClass)) {
if (!MetaModel::IsParentClass($sTargetObjectClass, $sFinalClass)) {
throw new Exception('The finalclass parameter should be a child class of the target object class');
}
} else {
$sFinalClass = $sTargetObjectClass;
}
// - Retrieving class attribute list
$aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sFinalClass, 'list');
// - Adding friendlyname attribute to the list is not already in it
$sTitleAttCode = 'friendlyname';
if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) {
$aAttCodes = array_merge([$sTitleAttCode], $aAttCodes);
}
// Retrieving results
// - Retrieving columns properties
$aColumnProperties = [];
foreach ($aAttCodes as $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sFinalClass, $sAttCode);
$aColumnProperties[$sAttCode] = [
'title' => $oAttDef->GetLabel(),
];
}
// Preparing response
$aData = $aData + [
'levelsProperties' => $aColumnProperties,
];
return new JsonResponse($aData);
}
/**
* Handles the regular (table) search from an attribute
*
@@ -737,6 +856,7 @@ class ObjectController extends BrickController
*/
public function SearchFromAttributeAction(Request $oRequest, $sTargetAttCode, $sHostObjectClass, $sHostObjectId = null)
{
$sFinalClass = $this->oRequestManipulatorHelper->ReadParam('finalclass', null, FILTER_UNSAFE_RAW);
/** @var array $aCombodoPortalInstanceConf */
$aCombodoPortalInstanceConf = $this->getParameter('combodo.portal.instance.conf');
@@ -826,9 +946,16 @@ class ObjectController extends BrickController
} else {
throw new Exception('Search from attribute can only apply on AttributeExternalKey or AttributeLinkedSet objects, '.get_class($oTargetAttDef).' given.');
}
if (utils::IsNotNullOrEmptyString($sFinalClass)) {
if (!MetaModel::IsParentClass($sTargetObjectClass, $sFinalClass)) {
throw new Exception('The finalclass parameter should be a child class of the target object class');
}
} else {
$sFinalClass = $sTargetObjectClass;
}
// - Retrieving class attribute list
$aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sTargetObjectClass, 'list');
$aAttCodes = ApplicationHelper::GetLoadedListFromClass($aCombodoPortalInstanceConf['lists'], $sFinalClass, 'list');
// - Adding friendlyname attribute to the list is not already in it
$sTitleAttCode = 'friendlyname';
if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodes)) {
@@ -838,10 +965,10 @@ class ObjectController extends BrickController
// - Retrieving scope search
// Note : This do NOT apply to custom fields as the portal administrator is not supposed to know which objects will be put in the templates.
// It is the responsibility of the template designer to write the right query so the user see only what he should.
$oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sTargetObjectClass, UR_ACTION_READ);
$oScopeSearch = $this->oScopeValidatorHelper->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sFinalClass, UR_ACTION_READ);
$aInternalParams = [];
if (($oScopeSearch === null) && ($oTargetAttDef->GetEditClass() !== 'CustomFields')) {
IssueLog::Info(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' has no scope query for '.$sTargetObjectClass.' class.');
IssueLog::Info(__METHOD__.' at line '.__LINE__.' : User #'.UserRights::GetUserId().' has no scope query for '.$sFinalClass.' class.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
@@ -855,7 +982,9 @@ class ObjectController extends BrickController
// Note : $oTemplateFieldSearch has been defined in the "Retrieving target object class from attcode" part, it is not available otherwise
$oSearch = $oTemplateFieldSearch;
}
if ($sFinalClass != $sTargetObjectClass) {
$oSearch->AddCondition('finalclass', $sFinalClass, '=');
}
// - Filtering objects to ignore
if (($aObjectIdsToIgnore !== null) && (is_array($aObjectIdsToIgnore))) {
//$oSearch->AddConditionExpression('id', $aObjectIdsToIgnore, 'NOT IN');
@@ -877,7 +1006,7 @@ class ObjectController extends BrickController
/** @noinspection SlowArrayOperationsInLoopInspection */
for ($i = 0; $i < count($aAttCodes); $i++) {
// Checking if the current attcode is an external key in order to search on the friendlyname
$oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $aAttCodes[$i]);
$oAttDef = MetaModel::GetAttributeDef($sFinalClass, $aAttCodes[$i]);
$sAttCode = (!$oAttDef->IsExternalKey()) ? $aAttCodes[$i] : $aAttCodes[$i].'_friendlyname';
// Building expression for the current attcode
// - For attributes that need conversion from their display value to storage value
@@ -933,38 +1062,25 @@ class ObjectController extends BrickController
}
}
// Retrieving results
// - Preparing object set
$oSet = new DBObjectSet($oSearch, [], $aInternalParams);
$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aAttCodes]);
$oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
// - Retrieving columns properties
$aColumnProperties = [];
foreach ($aAttCodes as $sAttCode) {
$oAttDef = MetaModel::GetAttributeDef($sTargetObjectClass, $sAttCode);
$oAttDef = MetaModel::GetAttributeDef($sFinalClass, $sAttCode);
$aColumnProperties[$sAttCode] = [
'title' => $oAttDef->GetLabel(),
];
}
// - Retrieving objects
$aItems = [];
while ($oItem = $oSet->Fetch()) {
$aItems[] = $this->PrepareObjectInformation($oItem, $aAttCodes);
}
// Preparing response
if ($bInitialPass) {
$aData = $aData + [
'sParentClassName' => Dict::S('Class:'.$sTargetObjectClass),
'form' => [
'id' => 'object_search_form_'.time(),
'title' => Dict::Format('Brick:Portal:Object:Search:Regular:Title', $oTargetAttDef->GetLabel()),
'title_complement' => MetaModel::GetName($sTargetObjectClass),
],
'aColumnProperties' => json_encode($aColumnProperties),
'aResults' => [
'aItems' => json_encode($aItems),
'iCount' => count($aItems),
],
'bMultipleSelect' => $oTargetAttDef->IsLinkSet(),
'aSource' => [
'sFormPath' => $sFormPath,
@@ -974,7 +1090,19 @@ class ObjectController extends BrickController
'sFormManagerData' => $sFormManagerData,
],
];
if (MetaModel::HasChildrenClasses($sTargetObjectClass)) {
$aEnumChildClasses = \MetaModel::EnumChildClasses($sTargetObjectClass);
$aChildClasses = [];
foreach ($aEnumChildClasses as $sClassName) {
$aChildClasses[$sClassName] = MetaModel::GetName($sClassName);
}
$aData = $aData + [
'bHasSubClasses' => true,
'aSubClasses' => $aChildClasses,
];
} else {
$aData = $aData + ['bHasSubClasses' => false];
}
if ($oRequest->isXmlHttpRequest()) {
$oResponse = $this->render($this->GetTemplatePath('modal'), $aData);
} else {
@@ -982,13 +1110,22 @@ class ObjectController extends BrickController
$oResponse = $this->render($this->GetTemplatePath('page'), $aData);
}
} else {
// Retrieving results
// - Preparing object set
$oSet = new DBObjectSet($oSearch, [], $aInternalParams);
$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aAttCodes]);
$oSet->SetLimit($iListLength, $iListLength * ($iPageNumber - 1));
// - Retrieving objects
$aItems = [];
while ($oItem = $oSet->Fetch()) {
$aItems[] = $this->PrepareObjectInformation($oItem, $aAttCodes);
}
$aData = $aData + [
'levelsProperties' => $aColumnProperties,
'data' => $aItems,
'recordsTotal' => $oSet->Count(),
'recordsFiltered' => $oSet->Count(),
];
$oResponse = new JsonResponse($aData);
}

View File

@@ -8,8 +8,19 @@
<div id="{{ sFormId }}">
{#<div class="form_alerts"></div>#}
{% if bHasSubClasses %}
<div class="form_field_label">
<label>{{ 'UI:SearchFor_Class'|dict_format('') }}</label>
<select id="finalclass{{ sFormId }}">
<option value="">{{ sParentClassName }}</option>
{% for key, sClassName in aSubClasses %}
<option value="{{ key }}">{{ sClassName }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="form_fields">
<table id="{{ sTableId }}" class="object-list table table-striped responsive" cellspacing="0" width="100%">
<table id="{{ sTableId }}" class="object-list table table-striped responsive">
<tbody>
</tbody>
</table>
@@ -26,13 +37,13 @@
<script type="text/javascript">
var oColumnProperties = {{ aColumnProperties|raw }};
var oRawDatas = {{ aResults.aItems|raw }};
var oTable;
// Used for ajax throttling
var iSearchThrottle = 600;
var oKeyTimeout;
var aKeyTimeoutFilteredKeys = [9, 16, 17, 18, 19, 27, 33, 34, 35, 36, 37, 38, 39, 40]; // Tab, Shift, Ctrl, Alt, Pause, Esc, Page Up/Down, Home, End, Left/Up/Right/Down arrows
// Used for form
var oSelectedItems = {};
// Show a loader inside the table
@@ -102,8 +113,8 @@
return aColumnsDefinition;
};
$(document).ready(function(){
var createDatatable = function (sUrl) {
showTableLoader();
// Note : Those options should be externalized in an library so we can use them on any DataTables for the portal.
// We would just have to override / complete the necessary elements
@@ -176,7 +187,7 @@
"processing": true,
"serverSide": true,
"ajax": {
"url": "{{ app.url_generator.generate('p_object_search_from_attribute', {'sTargetAttCode': sTargetAttCode, 'sHostObjectClass': sHostObjectClass, 'sHostObjectId': sHostObjectId, 'ar_token': sActionRulesToken})|raw }}",
"url": sUrl,
"type": "POST",
"data": function(d){
d.sFormPath = '{{ aSource.sFormPath }}';
@@ -271,8 +282,15 @@
}, iSearchThrottle);
}
});
// Shows a loader in the table when processing
return oTable;
}
$(document).ready(function () {
sUrl = "{{ app.url_generator.generate('p_object_search_from_attribute', {'sTargetAttCode': sTargetAttCode, 'sHostObjectClass': sHostObjectClass, 'sHostObjectId': sHostObjectId, 'ar_token': sActionRulesToken})|raw }}";
aColumnsDefinition = getColumnsDefinition();
oTable = createDatatable(sUrl);
showTableLoader();
// Shows a loader in the table when processing
$('#{{ sTableId }}').on('processing.dt', function(event, settings, processing){
if(processing === true)
{
@@ -306,4 +324,18 @@
$('#{{ sFormId }}').closest('.modal').find('.modal-footer').hide();
{% endif %}
});
///start of personalisation
{% if bHasSubClasses %}
$('#finalclass{{ sFormId }}').on('change', function () {
oTable.clear().destroy();
$('#{{ sTableId }}').empty();
sUrlAjax = "{{ app.url_generator.generate('p_object_search_from_attribute', {'sTargetAttCode': sTargetAttCode, 'sHostObjectClass': sHostObjectClass, 'sHostObjectId': sHostObjectId, 'ar_token': sActionRulesToken})|raw }}&finalclass=" + $('#finalclass{{ sFormId }}').val();
sUrlColumns = "{{ app.url_generator.generate('p_columns_from_attribute_with_class', {'sTargetAttCode': sTargetAttCode, 'sHostObjectClass': sHostObjectClass, 'sHostObjectId': sHostObjectId, 'ar_token': sActionRulesToken})|raw }}&finalclass=" + $('#finalclass{{ sFormId }}').val();
$.post(sUrlColumns, function (aResult) {
oColumnProperties = aResult.levelsProperties;
oTable = createDatatable(sUrlAjax);
});
});
{% endif %}
</script>