diff --git a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/association/DataAssociationManagementView.js b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/association/DataAssociationManagementView.js
index 356daf94a8..95f9adf814 100644
--- a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/association/DataAssociationManagementView.js
+++ b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/association/DataAssociationManagementView.js
@@ -70,8 +70,15 @@ define([
this.mapping = this.getCurrentMapping();
this.mappingSync = this.getSyncNow();
this.data.numRepresentativeProps = this.getNumRepresentativeProps();
- this.data.sourceProps = _.pluck(this.mapping.properties,"source").slice(0,this.data.numRepresentativeProps);
- this.data.targetProps = _.pluck(this.mapping.properties,"target").slice(0,this.data.numRepresentativeProps);
+ // Only keep properties that actually define a "source"/"target"; a leading
+ // undefined value would break the sample search query. See discussion #186.
+ // Use native Array filter/slice (lodash 3 _.first ignores the count arg).
+ this.data.sourceProps = _.pluck(this.mapping.properties, "source")
+ .filter(function(source){ return !!source; })
+ .slice(0, this.data.numRepresentativeProps);
+ this.data.targetProps = _.pluck(this.mapping.properties, "target")
+ .filter(function(target){ return !!target; })
+ .slice(0, this.data.numRepresentativeProps);
this.data.hideSingleRecordReconButton = mappingUtils.readOnlySituationalPolicy(this.mapping.policies);
this.data.reconAvailable = false;
diff --git a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/behaviors/SingleRecordReconciliationView.js b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/behaviors/SingleRecordReconciliationView.js
index b0845ad316..f40fc286a4 100644
--- a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/behaviors/SingleRecordReconciliationView.js
+++ b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/behaviors/SingleRecordReconciliationView.js
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
+ * Portions copyright 2026 3A Systems, LLC.
*/
define([
@@ -79,7 +80,13 @@ define([
},
setupSearch: function(){
- var autocompleteProps = _.pluck(this.data.mapping.properties,"source").slice(0,this.getNumRepresentativeProps());
+ // Only keep properties that actually define a "source"; otherwise the
+ // first representative property may be undefined and break the sample
+ // search query (empty _sortKeys= => HTTP 500). See discussion #186.
+ // Use native Array filter/slice (lodash 3 _.first ignores the count arg).
+ var autocompleteProps = _.pluck(this.data.mapping.properties, "source")
+ .filter(function(source){ return !!source; })
+ .slice(0, this.getNumRepresentativeProps());
mappingUtils.setupSampleSearch($("#findSampleSource",this.$el), this.data.mapping, autocompleteProps, _.bind(function(item) {
conf.globalData.testSyncSource = item;
diff --git a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/properties/AttributesGridView.js b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/properties/AttributesGridView.js
index e611f8a899..977004a180 100644
--- a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/properties/AttributesGridView.js
+++ b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/properties/AttributesGridView.js
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions copyright 2026 3A Systems, LLC.
*/
define([
@@ -111,7 +112,13 @@ define([
this.data.mapProps = mapProps;
if (this.data.usesDynamicSampleSource) {
- let autocompleteProps = _.pluck(this.model.mapping.properties,"source").slice(0, this.model.numRepresentativeProps);
+ // Only keep properties that actually define a "source"; otherwise the
+ // first representative property may be undefined and break the sample
+ // search query (empty _sortKeys= => HTTP 500). See discussion #186.
+ // Use native Array filter/slice (lodash 3 _.first ignores the count arg).
+ let autocompleteProps = _.pluck(this.model.mapping.properties, "source")
+ .filter((source) => !!source)
+ .slice(0, this.model.numRepresentativeProps);
mappingUtils.setupSampleSearch($("#findSampleSource", this.$el), this.model.mapping, autocompleteProps, (item) => {
item.IDMSampleMappingName = this.model.mapping.name;
diff --git a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/util/MappingUtils.js b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/util/MappingUtils.js
index 55b04ea265..b9278d7e20 100644
--- a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/util/MappingUtils.js
+++ b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/mapping/util/MappingUtils.js
@@ -65,11 +65,14 @@ define([
obj.setupSampleSearch = function(el, mapping, autocompleteProps, selectSuccessCallback){
var searchList,
- selectedItem;
+ selectedItem,
+ // Guard against empty/undefined entries (e.g. properties without a "source").
+ cleanProps = _.compact(autocompleteProps),
+ primaryProp = cleanProps[0];
el.selectize({
- valueField: autocompleteProps[0],
- searchField: autocompleteProps,
+ valueField: primaryProp,
+ searchField: cleanProps,
maxOptions: 10,
create: false,
onChange: function() {
@@ -96,18 +99,20 @@ define([
item: function(item, escape) {
selectedItem = item;
- return "
" +escape(item[autocompleteProps[0]]) +"
";
+ return "
" +escape(item[primaryProp]) +"
";
}
},
load: function(query, callback) {
- if (!query.length || query.length < 2 || !autocompleteProps.length) {
+ if (!query.length || query.length < 2 || !cleanProps.length) {
return callback();
}
- searchDelegate.searchResults(mapping.source, autocompleteProps, query).then(function(response) {
- if(response) {
+ searchDelegate.searchResults(mapping.source, cleanProps, query).then(function(response) {
+ if(response && response.length) {
searchList = response;
- callback([response]);
+ // searchResults already returns an array of records; pass it
+ // through as-is (selectize expects a flat array of options).
+ callback(response);
} else {
searchList = [];
diff --git a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/mapping/util/MappingUtilsTest.js b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/mapping/util/MappingUtilsTest.js
index dceb72f5b0..482f438378 100644
--- a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/mapping/util/MappingUtilsTest.js
+++ b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/mapping/util/MappingUtilsTest.js
@@ -1,5 +1,63 @@
define([
- "org/forgerock/openidm/ui/admin/mapping/util/MappingUtils"
-], function (MappingUtils) {
+ "jquery",
+ "sinon",
+ "org/forgerock/openidm/ui/admin/mapping/util/MappingUtils",
+ "org/forgerock/openidm/ui/common/delegates/SearchDelegate"
+], function ($, sinon, MappingUtils, SearchDelegate) {
QUnit.module('MappingUtils Tests');
+
+ QUnit.test("setupSampleSearch passes a flat array of records to the selectize load callback", function (assert) {
+ var done = assert.async(),
+ records = [{ email: "jsanchez@example.com", lastName: "Sanchez", firstName: "Jane" }],
+ capturedConfig,
+ selectizeStub = sinon.stub($.fn, "selectize", function (config) {
+ capturedConfig = config;
+ return this;
+ }),
+ searchStub = sinon.stub(SearchDelegate, "searchResults", function () {
+ return $.Deferred().resolve(records).promise();
+ });
+
+ try {
+ MappingUtils.setupSampleSearch(
+ $(""),
+ { source: "system/hr/account" },
+ ["email", "lastName", "firstName"],
+ function () {}
+ );
+
+ assert.equal(capturedConfig.valueField, "email", "valueField is the first non-empty prop");
+
+ capturedConfig.load("Sanchez", function (options) {
+ // Regression test: options must be the flat array of records, not [[...]].
+ assert.deepEqual(options, records, "load callback receives a flat array of records");
+ done();
+ });
+ } finally {
+ selectizeStub.restore();
+ searchStub.restore();
+ }
+ });
+
+ QUnit.test("setupSampleSearch ignores props without a source (compacts valueField/searchField)", function (assert) {
+ var capturedConfig,
+ selectizeStub = sinon.stub($.fn, "selectize", function (config) {
+ capturedConfig = config;
+ return this;
+ });
+
+ try {
+ MappingUtils.setupSampleSearch(
+ $(""),
+ { source: "managed/user" },
+ [undefined, "userName", "sn"],
+ function () {}
+ );
+
+ assert.equal(capturedConfig.valueField, "userName", "valueField falls back to first non-empty prop");
+ assert.deepEqual(capturedConfig.searchField, ["userName", "sn"], "searchField has no undefined entries");
+ } finally {
+ selectizeStub.restore();
+ }
+ });
});
\ No newline at end of file
diff --git a/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/delegates/SearchDelegate.js b/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/delegates/SearchDelegate.js
index 7f6a59c626..a8b34987f6 100644
--- a/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/delegates/SearchDelegate.js
+++ b/openidm-ui/openidm-ui-common/src/main/js/org/forgerock/openidm/ui/common/delegates/SearchDelegate.js
@@ -23,12 +23,41 @@ define([
var obj = new AbstractDelegate(constants.host + "/" + constants.context);
+ /**
+ * Builds the search URL for a resource query.
+ *
+ * Note: only the first non-empty property is used for "_sortKeys". When the
+ * supplied props array starts with empty/undefined values (e.g. a mapping
+ * whose first property has no "source"), an empty "_sortKeys=" must NOT be
+ * emitted, otherwise the backend (CREST/IDM) returns an HTTP 500 error.
+ *
+ * In addition, "_sortKeys" is omitted for system resources ("system/..."),
+ * because many connectors (e.g. the CSV connector) do not support
+ * server-side sorting and would fail the whole query. Sample searches are
+ * capped at a handful of results anyway, so the sort order is not required.
+ */
+ obj.buildSearchUrl = function (resource, props, searchString, comparisonOperator, additionalQuery, maxPageSize) {
+ var sortKey = _.find(props, function (p) { return !!p; }),
+ sortableResource = !/^\/?system\//.test(resource),
+ url = "/" + resource + "?";
+
+ if (sortKey && sortableResource) {
+ url += "_sortKeys=" + sortKey + "&";
+ }
+
+ // [a,b] => "a or (b)"; [a,b,c] => "a or (b or (c))"
+ url += "_pageSize=" + maxPageSize +
+ "&_queryFilter=" + obj.generateQueryFilter(props, searchString, additionalQuery, comparisonOperator);
+
+ return url;
+ };
+
obj.searchResults = function (resource, props, searchString, comparisonOperator, additionalQuery) {
var maxPageSize = 10;
return this.serviceCall({
"type": "GET",
- "url": "/" + resource + "?_sortKeys=" + props[0] + "&_pageSize=" + maxPageSize + "&_queryFilter=" + obj.generateQueryFilter(props, searchString, additionalQuery, comparisonOperator)// [a,b] => "a or (b)"; [a,b,c] => "a or (b or (c))"
+ "url": obj.buildSearchUrl(resource, props, searchString, comparisonOperator, additionalQuery, maxPageSize)
}).then(
function (qry) {
return _.take(qry.result, maxPageSize);//we never want more than 10 results from search in case _pageSize does not work
diff --git a/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/delegates/SearchDelegateTest.js b/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/delegates/SearchDelegateTest.js
index ced34370c9..ec7b7b1b89 100644
--- a/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/delegates/SearchDelegateTest.js
+++ b/openidm-ui/openidm-ui-common/src/test/qunit/tests/org/forgerock/openidm/ui/common/delegates/SearchDelegateTest.js
@@ -1,3 +1,18 @@
+/**
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Portions copyright 2026 3A Systems, LLC.
+ */
define([
"org/forgerock/openidm/ui/common/delegates/SearchDelegate"
], function (SearchDelegate) {
@@ -12,4 +27,44 @@ define([
assert.equal(SearchDelegate.generateQueryFilter(props, search), '(userName sw "test" or (givenName sw "test" or (sn sw "test")))', "Basic Query Filter Generated");
assert.equal(SearchDelegate.generateQueryFilter(props, search, additionalQuery, comparisonOperator), '((userName sw "test" or (givenName sw "test" or (sn sw "test"))) and (true))', "Complex Query Filter Generated");
});
+
+ QUnit.test("buildSearchUrl uses the first property as _sortKeys", function (assert) {
+ var url = SearchDelegate.buildSearchUrl("managed/user", ["userName", "sn"], "test", null, null, 10);
+
+ assert.equal(
+ url,
+ '/managed/user?_sortKeys=userName&_pageSize=10&_queryFilter=(userName sw "test" or (sn sw "test"))',
+ "_sortKeys is set to the first property"
+ );
+ });
+
+ QUnit.test("buildSearchUrl skips empty _sortKeys when the first property is missing (discussion #186)", function (assert) {
+ // Mapping whose first property has no "source" => leading undefined value.
+ var url = SearchDelegate.buildSearchUrl("managed/user", [undefined, "sn"], "test", null, null, 10);
+
+ assert.equal(url.indexOf("_sortKeys=&"), -1, "no empty _sortKeys= is emitted");
+ assert.equal(
+ url,
+ '/managed/user?_sortKeys=sn&_pageSize=10&_queryFilter=(sn sw "test")',
+ "_sortKeys falls back to the first non-empty property"
+ );
+ });
+
+ QUnit.test("buildSearchUrl omits _sortKeys entirely when no property is usable", function (assert) {
+ var url = SearchDelegate.buildSearchUrl("managed/user", [undefined, ""], "test", null, null, 10);
+
+ assert.equal(url.indexOf("_sortKeys"), -1, "no _sortKeys parameter is present at all");
+ assert.equal(url.indexOf("?_pageSize=10"), "/managed/user".length, "the query string starts directly with _pageSize");
+ });
+
+ QUnit.test("buildSearchUrl omits _sortKeys for system resources that may not support sorting", function (assert) {
+ var url = SearchDelegate.buildSearchUrl("system/hr/account", ["email", "lastName"], "Sanchez", null, null, 10);
+
+ assert.equal(url.indexOf("_sortKeys"), -1, "no _sortKeys parameter is sent to a system connector");
+ assert.equal(
+ url,
+ '/system/hr/account?_pageSize=10&_queryFilter=(email sw "Sanchez" or (lastName sw "Sanchez"))',
+ "system resource query has no _sortKeys but keeps the query filter"
+ );
+ });
});