Merge remote-tracking branch 'refs/remotes/origin/support/3.2' into develop

This commit is contained in:
jf-cbd
2024-07-04 13:56:53 +02:00
8 changed files with 131 additions and 126 deletions

View File

@@ -22,12 +22,12 @@ const CombodoCKEditorFeeds = {
}) })
.then(json => { .then(json => {
// ckeditor mandatory data // ckeditor mandatory data
json.data['search_data'].forEach(e => { json.search_data.forEach(e => {
e['name'] = e['friendlyname']; e['name'] = e['friendlyname'];
e['id'] = options['marker']+e['friendlyname']; e['id'] = options['marker']+e['friendlyname'];
}); });
// return searched data // return searched data
resolve( json.data['search_data']); resolve( json.search_data);
}); });
}, options.throttle); }, options.throttle);

View File

@@ -2334,121 +2334,13 @@ EOF
$oPage->add("</fieldset></div>"); $oPage->add("</fieldset></div>");
break; break;
// TODO 3.0.0: Move this to new ajax render controller? /**
* @internal
* @deprecated 3.2.0 N°7552 Use object.search_for_mentions route instead
*/
case 'cke_mentions': case 'cke_mentions':
$oPage->SetContentType('application/json'); $oController = new ObjectController();
$sMarker = utils::ReadParam('marker', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA); $oPage = $oController->OperationSearchForMentions();
$sNeedle = utils::ReadParam('needle', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
$sHostClass = utils::ReadParam('host_class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS);
$iHostId = (int)utils::ReadParam('host_id', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
// Check parameters
if ($sMarker === '') {
throw new Exception('Invalid parameters, marker must be specified.');
}
$aMentionsAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
if (isset($aMentionsAllowedClasses[$sMarker]) === false) {
throw new Exception('Invalid marker "'.$sMarker.'"');
}
$aMatches = array();
if ($sNeedle !== '') {
// Retrieve mentioned class from marker
$sMentionedClass = $aMentionsAllowedClasses[$sMarker];
if (MetaModel::IsValidClass($sMentionedClass) === false) {
throw new Exception('Invalid class "'.$sMentionedClass.'" for marker "'.$sMarker.'"');
}
// Base search used when no trigger configured
$oSearch = DBSearch::FromOQL("SELECT $sMentionedClass");
$aSearchParams = ['needle' => "%$sNeedle%"];
// Retrieve restricting scopes from triggers if any
if ((strlen($sHostClass) > 0) && ($iHostId > 0)) {
$oHostObj = MetaModel::GetObject($sHostClass, $iHostId);
$aSearchParams['this'] = $oHostObj;
$aTriggerMentionedSearches = [];
$aTriggerSetParams = array('class_list' => MetaModel::EnumParentClasses($sHostClass, ENUM_PARENT_CLASSES_ALL));
$oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), array(), $aTriggerSetParams);
/** @var \TriggerOnObjectMention $oTrigger */
while ($oTrigger = $oTriggerSet->Fetch()) {
$sTriggerMentionedOQL = $oTrigger->Get('mentioned_filter');
// No filter on mentioned objects, don't restrict the scope at all, it can be any object of $sMentionedClass
if (strlen($sTriggerMentionedOQL) === 0) {
$aTriggerMentionedSearches = [$oSearch];
break;
}
$oTriggerMentionedSearch = DBSearch::FromOQL($sTriggerMentionedOQL);
$sTriggerMentionedClass = $oTriggerMentionedSearch->GetClass();
// Filter is not about the mentioned class, don't mind it
if (is_a($sMentionedClass, $sTriggerMentionedClass, true) === false) {
continue;
}
$aTriggerMentionedSearches[] = $oTriggerMentionedSearch;
}
if (count($aTriggerMentionedSearches) > 0) {
$oSearch = new DBUnionSearch($aTriggerMentionedSearches);
}
}
$sSearchMainClassName = $oSearch->GetClass();
$sSearchMainClassAlias = $oSearch->GetClassAlias();
$sObjectImageAttCode = MetaModel::GetImageAttributeCode($sSearchMainClassName);
// Add condition to filter on the friendlyname
$oSearch->AddConditionExpression(
new BinaryExpression(new FieldExpression('friendlyname', $sSearchMainClassAlias), 'LIKE', new VariableExpression('needle'))
);
$oSet = new DBObjectSet($oSearch, [], $aSearchParams);
// Optimize fields to load
$aObjectAttCodesToLoad = [];
if (MetaModel::IsValidAttCode($sSearchMainClassName, $sObjectImageAttCode)) {
$aObjectAttCodesToLoad[] = $sObjectImageAttCode;
}
$oSet->OptimizeColumnLoad([$oSearch->GetClassAlias() => $aObjectAttCodesToLoad]);
$oSet->SetLimit(MetaModel::GetConfig()->Get('max_autocomplete_results'));
// Note: We have to this manually because of a bug in DBSearch not checking the user prefs. by default.
$oSet->SetShowObsoleteData(utils::ShowObsoleteData());
while ($oObject = $oSet->Fetch()) {
// Note $oObject finalclass might be different than $sMentionedClass
$sObjectClass = get_class($oObject);
$iObjectId = $oObject->GetKey();
$aMatch = [
'class' => $sObjectClass,
'id' => $iObjectId,
'friendlyname' => $oObject->Get('friendlyname'),
];
// Try to retrieve image for contact
if (!empty($sObjectImageAttCode)) {
/** @var \ormDocument $oImage */
$oImage = $oObject->Get($sObjectImageAttCode);
if (!$oImage->IsEmpty()) {
$aMatch['picture_style'] = "background-image: url('".$oImage->GetDisplayURL($sObjectClass, $iObjectId, $sObjectImageAttCode)."')";
$aMatch['initials'] = '';
} else {
// If no image found, fallback on initials
$aMatch['picture_style'] = '';
$aMatch['initials'] = utils::FormatInitialsForMedallion(utils::ToAcronym($oObject->Get('friendlyname')));
}
}
$aMatches[] = $aMatch;
}
}
$oPage->add(json_encode($aMatches));
break; break;
case 'custom_fields_update': case 'custom_fields_update':

View File

@@ -107,7 +107,7 @@ class CKEditorHelper
'minimumCharacters' => MetaModel::GetConfig()->Get('min_autocomplete_chars'), 'minimumCharacters' => MetaModel::GetConfig()->Get('min_autocomplete_chars'),
'feed_type' => 'ajax', 'feed_type' => 'ajax',
'feed_ajax_options' => [ 'feed_ajax_options' => [
'url' => utils::GetAbsoluteUrlAppRoot(). "pages/ajax.render.php?route=object.search&object_class=$sMentionClass&oql=SELECT $sMentionClass&search=", 'url' => utils::GetAbsoluteUrlAppRoot(). "pages/ajax.render.php?route=object.search_for_mentions&marker=".urlencode($sMentionMarker)."&needle=",
'throttle' => 500, 'throttle' => 500,
'marker' => $sMentionMarker, 'marker' => $sMentionMarker,
], ],

View File

@@ -217,12 +217,23 @@ class CaseLogEntryForm extends UIContentBlock
$this->oTextInput = new RichText(); $this->oTextInput = new RichText();
// Add the "host_class" to the mention endpoints so it can filter objects regarding the triggers // Add the "host_class" to the mention endpoints so it can filter objects regarding the triggers
// Mind that `&needle=` must be ending the endpoint URL in order for the JS plugin to append the needle string
$aConfig = $this->oTextInput->GetConfig(); $aConfig = $this->oTextInput->GetConfig();
if (isset($aConfig['mentions'])) { if (isset($aConfig['mention']['feeds'])) {
foreach ($aConfig['mentions'] as $iIdx => $aData) { foreach ($aConfig['mention']['feeds'] as $iIdx => $aData) {
$sFeed = $aConfig['mentions'][$iIdx]['feed']; $sFeed = $aConfig['mention']['feeds'][$iIdx]['feed_ajax_options']['url'];
// Remove existing "needle" parameter
$sFeed = str_replace('&needle=', '', $sFeed);
// Add new parameters
$sFeed = utils::AddParameterToUrl($sFeed, 'host_class', $this->GetObjectClass()); $sFeed = utils::AddParameterToUrl($sFeed, 'host_class', $this->GetObjectClass());
$aConfig['mentions'][$iIdx]['feed'] = utils::AddParameterToUrl($sFeed, 'host_id', $this->GetObjectId()); $sFeed = utils::AddParameterToUrl($sFeed, 'host_id', $this->GetObjectId());
// Re-append "needle" parameter
$sFeed = utils::AddParameterToUrl($sFeed, 'needle', '');
$aConfig['mention']['feeds'][$iIdx]['feed_ajax_options']['url'] = $sFeed;
} }
} }
$this->oTextInput->SetConfig($aConfig); $this->oTextInput->SetConfig($aConfig);

View File

@@ -53,7 +53,7 @@ class BlockList extends UIContentBlock
{ {
return '$("#'.$this->sId.'").block(); return '$("#'.$this->sId.'").block();
$.post("ajax.render.php?operation=refreshDashletList", $.post("ajax.render.php?operation=refreshDashletList",
{ style: "list", filter: "'.$this->sFilter.'", extra_params: '.json_encode($this->aExtraParams).' }, { style: "list", filter: '.json_encode($this->sFilter).', extra_params: '.json_encode($this->aExtraParams).' },
function(data){ function(data){
$("#'.$this->sId.'") $("#'.$this->sId.'")
.empty() .empty()

View File

@@ -21,6 +21,10 @@ use Combodo\iTop\Controller\AbstractController;
use Combodo\iTop\Service\Base\ObjectRepository; use Combodo\iTop\Service\Base\ObjectRepository;
use Combodo\iTop\Service\Router\Router; use Combodo\iTop\Service\Router\Router;
use CoreCannotSaveObjectException; use CoreCannotSaveObjectException;
use DBObjectSearch;
use DBObjectSet;
use DBSearch;
use DBUnionSearch;
use DeleteException; use DeleteException;
use Dict; use Dict;
use Exception; use Exception;
@@ -795,10 +799,12 @@ JS;
* Search objects via an oql and a friendly name search string * Search objects via an oql and a friendly name search string
* *
* @return JsonPage * @return JsonPage
* @used-by LinkedSet attribute when in tag display
*/ */
public function OperationSearch(): JsonPage public function OperationSearch(): JsonPage
{ {
$oPage = new JsonPage(); $oPage = new JsonPage();
$oPage->SetOutputDataOnly(true);
// Retrieve query params // Retrieve query params
$sObjectClass = utils::ReadParam('object_class', '', false, utils::ENUM_SANITIZATION_FILTER_STRING); $sObjectClass = utils::ReadParam('object_class', '', false, utils::ENUM_SANITIZATION_FILTER_STRING);
@@ -831,6 +837,95 @@ JS;
]); ]);
} }
public function OperationSearchForMentions(): JsonPage
{
$oPage = new JsonPage();
$oPage->SetOutputDataOnly(true);
$sMarker = utils::ReadParam('marker', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
$sNeedle = utils::ReadParam('needle', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
$sHostClass = utils::ReadParam('host_class', '', false, utils::ENUM_SANITIZATION_FILTER_CLASS);
$iHostId = (int) utils::ReadParam('host_id', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
// Check parameters
if (utils::IsNullOrEmptyString($sMarker)) {
throw new ApplicationException('Invalid parameters, marker must be specified.');
}
if (utils::IsNullOrEmptyString($sNeedle)) {
throw new ApplicationException('Invalid parameters, needle must be specified.');
}
$aMentionsAllowedClasses = MetaModel::GetConfig()->Get('mentions.allowed_classes');
if (isset($aMentionsAllowedClasses[$sMarker]) === false) {
throw new ApplicationException('Invalid marker "'.$sMarker.'"');
}
$aMatches = array();
// Retrieve mentioned class from marker
$sMentionedClass = $aMentionsAllowedClasses[$sMarker];
if (MetaModel::IsValidClass($sMentionedClass) === false) {
throw new ApplicationException('Invalid class "'.$sMentionedClass.'" for marker "'.$sMarker.'"');
}
// Base search used when no trigger configured
$oSearch = DBSearch::FromOQL("SELECT $sMentionedClass");
$aSearchParams = ['needle' => "%$sNeedle%"];
// Retrieve restricting scopes from triggers if any
if (utils::IsNotNullOrEmptyString($sHostClass) && ($iHostId > 0)) {
$oHostObj = MetaModel::GetObject($sHostClass, $iHostId);
$aSearchParams['this'] = $oHostObj;
$aTriggerMentionedSearches = [];
$aTriggerSetParams = array('class_list' => MetaModel::EnumParentClasses($sHostClass, ENUM_PARENT_CLASSES_ALL));
$oTriggerSet = new DBObjectSet(DBObjectSearch::FromOQL("SELECT TriggerOnObjectMention AS t WHERE t.target_class IN (:class_list)"), array(), $aTriggerSetParams);
/** @var \TriggerOnObjectMention $oTrigger */
while ($oTrigger = $oTriggerSet->Fetch()) {
$sTriggerMentionedOQL = $oTrigger->Get('mentioned_filter');
// No filter on mentioned objects, don't restrict the scope at all, it can be any object of $sMentionedClass
if (utils::IsNullOrEmptyString($sTriggerMentionedOQL)) {
$aTriggerMentionedSearches = [$oSearch];
break;
}
$oTriggerMentionedSearch = DBSearch::FromOQL($sTriggerMentionedOQL);
$sTriggerMentionedClass = $oTriggerMentionedSearch->GetClass();
// Filter is not about the mentioned class, don't mind it
if (is_a($sMentionedClass, $sTriggerMentionedClass, true) === false) {
continue;
}
$aTriggerMentionedSearches[] = $oTriggerMentionedSearch;
}
if (count($aTriggerMentionedSearches) > 0) {
$oSearch = new DBUnionSearch($aTriggerMentionedSearches);
}
}
$sSearchMainClassName = $oSearch->GetClass();
$sSearchMainClassAlias = $oSearch->GetClassAlias();
$sObjectImageAttCode = MetaModel::GetImageAttributeCode($sSearchMainClassName);
// Optimize fields to load
$aObjectAttCodesToLoad = [];
if (MetaModel::IsValidAttCode($sSearchMainClassName, $sObjectImageAttCode)) {
$aObjectAttCodesToLoad[] = $sObjectImageAttCode;
}
$aResult = ObjectRepository::SearchFromOql($sSearchMainClassName, $aObjectAttCodesToLoad, $oSearch->ToOQL(), $sNeedle, $oHostObj, MetaModel::GetConfig()->Get('max_autocomplete_results'));
return $oPage->SetData([
'search_data' => $aResult,
'success' => $aResult !== null,
]);
}
/** /**
* OperationGet. * OperationGet.
* *

View File

@@ -79,10 +79,12 @@ class ObjectRepository
* @param string $sOql Oql expression * @param string $sOql Oql expression
* @param string $sSearch Friendly name search string * @param string $sSearch Friendly name search string
* @param DBObject|null $oThisObject This object reference for oql * @param DBObject|null $oThisObject This object reference for oql
* @param int $iLimit Limit results to the $iLimit first elements
* *
* @return array|null * @return array|null
* @since 3.2.0 Add $iLimit parameter
*/ */
public static function SearchFromOql(string $sObjectClass, array $aFieldsToLoad, string $sOql, string $sSearch, DBObject $oThisObject = null): ?array public static function SearchFromOql(string $sObjectClass, array $aFieldsToLoad, string $sOql, string $sSearch, DBObject $oThisObject = null, int $iLimit = 0): ?array
{ {
try { try {
@@ -94,6 +96,11 @@ class ObjectRepository
// Create db set from db search // Create db set from db search
$oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], ['this' => $oThisObject]); $oDbObjectSet = new DBObjectSet($oDbObjectSearch, [], ['this' => $oThisObject]);
// Limit results
if ($iLimit > 0) {
$oDbObjectSet->SetLimit($iLimit);
}
// return object array // return object array
return ObjectRepository::DBSetToObjectArray($oDbObjectSet, $sObjectClass, $aFieldsToLoad); return ObjectRepository::DBSetToObjectArray($oDbObjectSet, $sObjectClass, $aFieldsToLoad);
} }

View File

@@ -108,8 +108,8 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
// Handle errors // Handle errors
if(!me.settings.hasError){ if(!me.settings.hasError){
me.toggleErrorClass(!res.data.success); me.toggleErrorClass(!res.success);
if(!res.data.success) return; if(!res.success) return;
} }
// Retrieve current input value // Retrieve current input value
@@ -120,7 +120,7 @@ let oWidget{{ oUIBlock.GetId() }} = $('#{{ oUIBlock.GetId() }}').selectize({
me.optionsBeforeFilter = options; me.optionsBeforeFilter = options;
options = options.filter(item => (typeof(item.force) !== "undefined" && item.force === true) || aSelectedItems.includes(item['{{ oDataProvider.GetDataValueField() }}'])); options = options.filter(item => (typeof(item.force) !== "undefined" && item.force === true) || aSelectedItems.includes(item['{{ oDataProvider.GetDataValueField() }}']));
// Merge kept and new values // Merge kept and new values
options = $.merge(options, res.data.search_data); options = $.merge(options, res.search_data);
// Compute groups // Compute groups
$.each(options, function(index, value) { $.each(options, function(index, value) {
me.addOptionGroup(value['{{ oDataProvider.GetGroupField() }}'], { me.addOptionGroup(value['{{ oDataProvider.GetGroupField() }}'], {