group_setting_pill: Add code for showing typeahead.

This commit adds code to show typeahead for group setting
pill container. We add a separate function as we only
want to show groups and users in the typeahead and the
options are also sorted in a different order compared
to other typeaheads.
This commit is contained in:
Sahil Batra 2024-08-29 19:39:06 +05:30 committed by Tim Abbott
parent 053686669a
commit 8068b6e55e
8 changed files with 617 additions and 15 deletions

View File

@ -5,10 +5,12 @@ import render_input_pill from "../templates/input_pill.hbs";
import * as group_permission_settings from "./group_permission_settings";
import * as input_pill from "./input_pill";
import type {InputPillConfig} from "./input_pill";
import * as pill_typeahead from "./pill_typeahead";
import type {GroupSettingPill, GroupSettingPillContainer} from "./typeahead_helper";
import * as user_group_pill from "./user_group_pill";
import type {UserGroupPill} from "./user_group_pill";
import * as user_groups from "./user_groups";
import type {UserGroup} from "./user_groups";
import * as user_pill from "./user_pill";
function check_group_allowed_for_setting(group_item: UserGroupPill, setting_name: string): boolean {
@ -102,3 +104,22 @@ export function create_pills(
});
return pill_widget;
}
export function set_up_pill_typeahead({
pill_widget,
$pill_container,
opts,
}: {
pill_widget: GroupSettingPillContainer;
$pill_container: JQuery;
opts: {
setting_name: string;
group: UserGroup | undefined;
};
}): void {
pill_typeahead.set_up_group_setting_typeahead(
$pill_container.find(".input"),
pill_widget,
opts,
);
}

View File

@ -8,9 +8,10 @@ import type {User} from "./people";
import * as stream_pill from "./stream_pill";
import type {StreamPillData, StreamPillWidget} from "./stream_pill";
import * as typeahead_helper from "./typeahead_helper";
import type {CombinedPillContainer} from "./typeahead_helper";
import type {CombinedPillContainer, GroupSettingPillContainer} from "./typeahead_helper";
import * as user_group_pill from "./user_group_pill";
import type {UserGroupPillData} from "./user_group_pill";
import type {UserGroup} from "./user_groups";
import * as user_pill from "./user_pill";
import type {UserPillData, UserPillWidget} from "./user_pill";
@ -26,6 +27,7 @@ function group_matcher(query: string, item: UserGroupPillData): boolean {
}
type TypeaheadItem = UserGroupPillData | StreamPillData | UserPillData;
type GroupSettingTypeaheadItem = UserGroupPillData | UserPillData;
export function set_up_user(
$input: JQuery,
@ -127,6 +129,87 @@ export function set_up_stream(
});
}
export function set_up_group_setting_typeahead(
$input: JQuery,
pills: GroupSettingPillContainer,
opts: {
setting_name: string;
group: UserGroup | undefined;
},
): void {
const bootstrap_typeahead_input: TypeaheadInputElement = {
$element: $input,
type: "contenteditable",
};
new Typeahead(bootstrap_typeahead_input, {
dropup: true,
source(_query: string): GroupSettingTypeaheadItem[] {
let source: GroupSettingTypeaheadItem[] = [];
source = user_group_pill.typeahead_source(pills, opts.setting_name);
source = [...source, ...user_pill.typeahead_source(pills, true)];
return source;
},
highlighter_html(item: GroupSettingTypeaheadItem, _query: string): string {
if (item.type === "user_group") {
return typeahead_helper.render_user_group(item);
}
assert(item.type === "user");
return typeahead_helper.render_person(item);
},
matcher(item: GroupSettingTypeaheadItem, query: string): boolean {
query = query.toLowerCase();
query = query.replaceAll("\u00A0", " ");
let matches = false;
if (item.type === "user_group") {
matches = matches || group_matcher(query, item);
}
if (item.type === "user") {
matches = matches || person_matcher(query, item);
}
return matches;
},
sorter(matches: GroupSettingTypeaheadItem[], query: string): GroupSettingTypeaheadItem[] {
const users: UserPillData[] = [];
for (const match of matches) {
if (match.type === "user" && people.is_known_user_id(match.user.user_id)) {
users.push(match);
}
}
const groups: UserGroupPillData[] = [];
for (const match of matches) {
if (match.type === "user_group") {
groups.push(match);
}
}
return typeahead_helper.sort_group_setting_options({
users,
query,
groups,
target_group: opts.group,
});
},
updater(item: GroupSettingTypeaheadItem, _query: string): undefined {
if (item.type === "user_group") {
user_group_pill.append_user_group(item, pills);
} else if (item.type === "user" && people.is_known_user_id(item.user.user_id)) {
user_pill.append_user(item.user, pills);
}
$input.trigger("focus");
},
stopAdvance: true,
helpOnEmptyStrings: true,
hideOnEmptyAfterBackspace: true,
});
}
export function set_up_combined(
$input: JQuery,
pills: CombinedPillContainer,

View File

@ -23,6 +23,7 @@ import type {StreamPill, StreamPillData} from "./stream_pill";
import type {StreamSubscription} from "./sub_store";
import type {UserGroupPill, UserGroupPillData} from "./user_group_pill";
import * as user_groups from "./user_groups";
import type {UserGroup} from "./user_groups";
import type {UserPill, UserPillData} from "./user_pill";
import * as user_status from "./user_status";
import type {UserStatusEmojiInfo} from "./user_status";
@ -555,6 +556,153 @@ export function sort_recipients<UserType extends UserOrMentionPillData | UserPil
return recipients.slice(0, max_num_items);
}
export function compare_setting_options(
option_a: UserPillData | UserGroupPillData,
option_b: UserPillData | UserGroupPillData,
target_group: UserGroup | undefined,
): number {
if (option_a.type === "user_group" && option_b.type === "user") {
return -1;
}
if (option_b.type === "user_group" && option_a.type === "user") {
return 1;
}
if (option_a.type === "user_group" && option_b.type === "user_group") {
const user_group_a = user_groups.get_user_group_from_id(option_a.id);
const user_group_b = user_groups.get_user_group_from_id(option_b.id);
if (user_group_a.is_system_group && !user_group_b.is_system_group) {
return -1;
}
if (user_group_b.is_system_group && !user_group_a.is_system_group) {
return 1;
}
if (user_group_a.name < user_group_b.name) {
return -1;
}
return 1;
}
assert(option_a.type === "user");
assert(option_b.type === "user");
if (target_group !== undefined) {
if (
!target_group.members.has(option_a.user.user_id) &&
target_group.members.has(option_b.user.user_id)
) {
return 1;
}
if (
target_group.members.has(option_a.user.user_id) &&
!target_group.members.has(option_b.user.user_id)
) {
return -1;
}
}
if (option_a.user.full_name < option_b.user.full_name) {
return -1;
} else if (option_a.user.full_name === option_b.user.full_name) {
return 0;
}
return 1;
}
export function sort_group_setting_options({
users,
query,
groups,
target_group,
}: {
users: UserPillData[];
query: string;
groups: UserGroupPillData[];
target_group: UserGroup | undefined;
}): (UserPillData | UserGroupPillData)[] {
function sort_group_setting_items(
objs: (UserPillData | UserGroupPillData)[],
): (UserPillData | UserGroupPillData)[] {
objs.sort((option_a, option_b) =>
compare_setting_options(option_a, option_b, target_group),
);
return objs;
}
const users_name_results = typeahead.triage_raw(query, users, (p) => p.user.full_name);
const email_results = typeahead.triage_raw(
query,
users_name_results.no_matches,
(p) => p.user.email,
);
const groups_results = typeahead.triage_raw(query, groups, (g) =>
user_groups.get_display_group_name(g.name),
);
const exact_matches = sort_group_setting_items([
...groups_results.exact_matches,
...users_name_results.exact_matches,
...email_results.exact_matches,
]);
const prefix_matches = sort_group_setting_items([
...groups_results.begins_with_case_sensitive_matches,
...groups_results.begins_with_case_insensitive_matches,
...users_name_results.begins_with_case_sensitive_matches,
...users_name_results.begins_with_case_insensitive_matches,
...email_results.begins_with_case_sensitive_matches,
...email_results.begins_with_case_insensitive_matches,
]);
const word_boundary_matches = sort_group_setting_items([
...groups_results.word_boundary_matches,
...users_name_results.word_boundary_matches,
...email_results.word_boundary_matches,
]);
const no_matches = sort_group_setting_items([
...groups_results.no_matches,
...email_results.no_matches,
]);
const getters: {
getter: (UserPillData | UserGroupPillData)[];
}[] = [
{
getter: exact_matches,
},
{
getter: prefix_matches,
},
{
getter: word_boundary_matches,
},
{
getter: no_matches,
},
];
const setting_options: (UserPillData | UserGroupPillData)[] = [];
for (const getter of getters) {
if (setting_options.length >= MAX_ITEMS) {
break;
}
for (const item of getter.getter) {
setting_options.push(item);
}
}
return setting_options.slice(0, MAX_ITEMS);
}
type SlashCommand = {
name: string;
};

View File

@ -1,7 +1,11 @@
import {$t_html} from "./i18n";
import type {InputPillContainer} from "./input_pill";
import * as people from "./people";
import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper";
import type {
CombinedPill,
CombinedPillContainer,
GroupSettingPillContainer,
} from "./typeahead_helper";
import type {UserGroup} from "./user_groups";
import * as user_groups from "./user_groups";
@ -75,7 +79,10 @@ function get_group_members(user_group: UserGroup): number[] {
return user_ids.filter((user_id) => people.is_person_active(user_id));
}
export function append_user_group(group: UserGroup, pill_widget: CombinedPillContainer): void {
export function append_user_group(
group: UserGroup,
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
): void {
pill_widget.appendValidatedData({
type: "user_group",
group_id: group.id,
@ -84,22 +91,32 @@ export function append_user_group(group: UserGroup, pill_widget: CombinedPillCon
pill_widget.clear_text();
}
export function get_group_ids(pill_widget: CombinedPillContainer): number[] {
export function get_group_ids(
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
): number[] {
const items = pill_widget.items();
return items.flatMap((item) => (item.type === "user_group" ? item.group_id : []));
}
export function filter_taken_groups(
items: UserGroup[],
pill_widget: CombinedPillContainer,
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
): UserGroup[] {
const taken_group_ids = get_group_ids(pill_widget);
items = items.filter((item) => !taken_group_ids.includes(item.id));
return items;
}
export function typeahead_source(pill_widget: CombinedPillContainer): UserGroupPillData[] {
const groups = user_groups.get_realm_user_groups();
export function typeahead_source(
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
setting_name?: string,
): UserGroupPillData[] {
let groups;
if (setting_name !== undefined) {
groups = user_groups.get_realm_user_groups_for_setting(setting_name, "group", true);
} else {
groups = user_groups.get_realm_user_groups();
}
return filter_taken_groups(groups, pill_widget).map((user_group) => ({
...user_group,
type: "user_group",

View File

@ -133,6 +133,14 @@ export function get_realm_user_groups(include_deactivated = false): UserGroup[]
});
}
// This is only used for testing currently, but would be used in
// future when we use system groups more and probably show them
// in the UI as well.
export function get_all_realm_user_groups(): UserGroup[] {
const user_groups = [...user_group_by_id_dict.values()].sort((a, b) => a.id - b.id);
return user_groups;
}
export function get_user_groups_allowed_to_mention(): UserGroup[] {
const user_groups = get_realm_user_groups();
return user_groups.filter((group) => {
@ -342,7 +350,7 @@ function get_display_name_for_system_group_option(setting_name: string, name: st
export function check_system_user_group_allowed_for_setting(
group_name: string,
group_setting_config: GroupPermissionSetting,
for_new_settings_ui = false,
for_new_settings_ui: boolean,
): boolean {
const {
allow_internet_group,
@ -390,6 +398,7 @@ export function check_system_user_group_allowed_for_setting(
export function get_realm_user_groups_for_setting(
setting_name: string,
setting_type: "realm" | "stream" | "group",
for_new_settings_ui = false,
): UserGroup[] {
const group_setting_config = group_permission_settings.get_group_permission_setting_config(
setting_name,
@ -402,7 +411,11 @@ export function get_realm_user_groups_for_setting(
const system_user_groups = settings_config.system_user_groups_list
.filter((group) =>
check_system_user_group_allowed_for_setting(group.name, group_setting_config),
check_system_user_group_allowed_for_setting(
group.name,
group_setting_config,
for_new_settings_ui,
),
)
.map((group) => {
const user_group = get_user_group_from_name(group.name);

View File

@ -7,7 +7,11 @@ import * as input_pill from "./input_pill";
import type {User} from "./people";
import * as people from "./people";
import {realm} from "./state_data";
import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper";
import type {
CombinedPill,
CombinedPillContainer,
GroupSettingPillContainer,
} from "./typeahead_helper";
import * as user_status from "./user_status";
// This will be used for pills for things like composing
@ -98,7 +102,7 @@ export function get_email_from_item(item: UserPill): string {
export function append_person(opts: {
person: User;
pill_widget: UserPillWidget | CombinedPillContainer;
pill_widget: UserPillWidget | CombinedPillContainer | GroupSettingPillContainer;
}): void {
const person = opts.person;
const pill_widget = opts.pill_widget;
@ -119,7 +123,9 @@ export function append_person(opts: {
pill_widget.clear_text();
}
export function get_user_ids(pill_widget: UserPillWidget | CombinedPillContainer): number[] {
export function get_user_ids(
pill_widget: UserPillWidget | CombinedPillContainer | GroupSettingPillContainer,
): number[] {
const items = pill_widget.items();
return items.flatMap((item) => (item.type === "user" ? (item.user_id ?? []) : [])); // be defensive about undefined users
}
@ -138,7 +144,7 @@ export function has_unconverted_data(pill_widget: UserPillWidget): boolean {
}
export function typeahead_source(
pill_widget: UserPillWidget | CombinedPillContainer,
pill_widget: UserPillWidget | CombinedPillContainer | GroupSettingPillContainer,
exclude_bots?: boolean,
): UserPillData[] {
const users = exclude_bots ? people.get_realm_active_human_users() : people.get_realm_users();
@ -147,14 +153,17 @@ export function typeahead_source(
export function filter_taken_users(
items: User[],
pill_widget: UserPillWidget | CombinedPillContainer,
pill_widget: UserPillWidget | CombinedPillContainer | GroupSettingPillContainer,
): User[] {
const taken_user_ids = get_user_ids(pill_widget);
items = items.filter((item) => !taken_user_ids.includes(item.user_id));
return items;
}
export function append_user(user: User, pills: UserPillWidget | CombinedPillContainer): void {
export function append_user(
user: User,
pills: UserPillWidget | CombinedPillContainer | GroupSettingPillContainer,
): void {
if (user) {
append_person({
pill_widget: pills,

View File

@ -6,10 +6,12 @@ const {zrequire, mock_esm} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const blueslip = require("./lib/zblueslip");
const $ = require("./lib/zjquery");
const {page_params, realm} = require("./lib/zpage_params");
const noop = function () {};
const bootstrap_typeahead = mock_esm("../src/bootstrap_typeahead");
const group_permission_setting = mock_esm("../src/group_permission_settings");
const input_pill = zrequire("input_pill");
const pill_typeahead = zrequire("pill_typeahead");
@ -22,6 +24,7 @@ const typeahead_helper = zrequire("typeahead_helper");
// set global test variables.
let sort_recipients_called = false;
let sort_streams_called = false;
let sort_group_setting_options_called = false;
const $fake_rendered_person = $.create("fake-rendered-person");
const $fake_rendered_stream = $.create("fake-rendered-stream");
const $fake_rendered_group = $.create("fake-rendered-group");
@ -543,3 +546,170 @@ run_test("set_up_combined", ({mock_template, override, override_rewire}) => {
pill_typeahead.set_up_combined($fake_input, $pill_widget, {});
assert.ok(!input_pill_typeahead_called);
});
run_test("set_up_group_setting_typeahead", ({mock_template, override, override_rewire}) => {
override_rewire(typeahead_helper, "render_person", () => $fake_rendered_person);
override_rewire(typeahead_helper, "render_user_group", () => $fake_rendered_group);
override_rewire(typeahead_helper, "sort_group_setting_options", () => {
sort_group_setting_options_called = true;
});
mock_template("input_pill.hbs", true, (_data, html) => html);
let input_pill_typeahead_called = false;
const $fake_input = $.create(".input");
$fake_input.before = noop;
const $container = $.create(".pill-container");
$container.find = () => $fake_input;
const $pill_widget = input_pill.create({
$container,
create_item_from_text: noop,
get_text_from_item: noop,
get_display_value_from_item: noop,
});
group_permission_setting.get_group_permission_setting_config = (setting_name, setting_type) => {
assert.equal(setting_name, "can_manage_group");
assert.equal(setting_type, "group");
// This is not same as the original config for can_manage_group
// setting, but is set in such a way that we need to create minimum
// system groups.
return {
require_system_group: false,
allow_internet_group: false,
allow_owners_group: false,
allow_nobody_group: true,
allow_everyone_group: false,
allowed_system_groups: ["role:moderators", "role:nobody", "role:fullmembers"],
};
};
const moderators_system_group = {
name: "role:moderators",
id: 3,
description: "Moderators",
members: [],
is_system_group: true,
};
const nobody_system_group = {
name: "role:nobody",
id: 4,
description: "Nobody",
members: [],
is_system_group: true,
};
const full_members_system_group = {
name: "role:fullmembers",
id: 5,
description: "Full members",
members: [],
is_system_group: true,
};
user_groups.add(moderators_system_group);
user_groups.add(nobody_system_group);
user_groups.add(full_members_system_group);
const moderators_item = user_group_item(moderators_system_group);
const system_group_items = [moderators_item];
page_params.development_environment = true;
realm.realm_waiting_period_threshold = 0;
override(bootstrap_typeahead, "Typeahead", (input_element, config) => {
assert.equal(input_element.$element, $fake_input);
assert.ok(config.dropup);
assert.ok(config.stopAdvance);
assert.equal(typeof config.source, "function");
assert.equal(typeof config.highlighter_html, "function");
assert.equal(typeof config.matcher, "function");
assert.equal(typeof config.sorter, "function");
assert.equal(typeof config.updater, "function");
// test queries
const person_query = "me";
const group_query = "test";
(function test_highlighter() {
// If user is also allowed along with user_group
// then we should check that each of them rendered correctly.
assert.equal(config.highlighter_html(testers_item, group_query), $fake_rendered_group);
assert.equal(config.highlighter_html(me_item, person_query), $fake_rendered_person);
})();
(function test_matcher() {
let result;
// group query, with correct item.
result = config.matcher(testers_item, group_query);
assert.ok(result);
// group query, with wrong item.
result = config.matcher(admins_item, group_query);
assert.ok(!result);
// person query with correct item.
result = config.matcher(me_item, person_query);
assert.ok(result);
// person query with wrong item.
result = config.matcher(jill_item, person_query);
assert.ok(!result);
})();
(function test_sorter() {
sort_group_setting_options_called = false;
config.sorter([testers_item], group_query);
assert.ok(sort_group_setting_options_called);
sort_group_setting_options_called = false;
config.sorter([me_item], person_query);
assert.ok(sort_group_setting_options_called);
})();
(function test_source() {
let expected_result = [];
let actual_result = [];
function is_group(item) {
return item.members;
}
const result = config.source(person_query);
actual_result = result
.map((item) => {
if (is_group(item)) {
return item.id;
}
return item.user_id;
})
.filter(Boolean);
expected_result = [...expected_result, ...system_group_items, ...group_items];
expected_result = [...expected_result, ...person_items];
expected_result = expected_result
.map((item) => {
if (is_group(item)) {
return item.id;
}
return item.user_id;
})
.filter(Boolean);
assert.deepEqual(actual_result, expected_result);
})();
(function test_updater() {
function number_of_pills() {
const pills = $pill_widget.items();
return pills.length;
}
assert.equal(number_of_pills(), 0);
config.updater(me_item, person_query);
assert.equal(number_of_pills(), 1);
config.updater(testers_item, group_query);
assert.equal(number_of_pills(), 2);
})();
input_pill_typeahead_called = true;
});
const opts = {
setting_name: "can_manage_group",
group: testers,
};
pill_typeahead.set_up_group_setting_typeahead($fake_input, $pill_widget, opts);
assert.ok(input_pill_typeahead_called);
});

View File

@ -22,6 +22,7 @@ const pygments_data = zrequire("pygments_data");
const util = zrequire("util");
const ct = zrequire("composebox_typeahead");
const th = zrequire("typeahead_helper");
const user_groups = zrequire("user_groups");
let next_id = 0;
@ -40,6 +41,10 @@ function broadcast_item(user) {
return {type: "broadcast", user};
}
function user_group_item(user_group) {
return {type: "user_group", ...user_group};
}
const a_bot = {
email: "a_bot@zulip.com",
full_name: "A Zulip test bot",
@ -124,6 +129,45 @@ stream_data.create_streams([dev_sub, linux_sub]);
stream_data.add_sub(dev_sub);
stream_data.add_sub(linux_sub);
const bob_system_group = {
id: 1,
name: "Bob system group",
description: "",
members: new Set([]),
is_system_group: true,
};
const bob_system_group_item = user_group_item(bob_system_group);
const bob_group = {
id: 2,
name: "Bob group",
description: "",
members: new Set([]),
is_system_group: false,
};
const bob_group_item = user_group_item(bob_group);
const second_bob_group = {
id: 3,
name: "bob 2 group",
description: "",
members: new Set([b_user_2.user_id]),
is_system_group: false,
};
const admins_group = {
id: 4,
name: "Admins of zulip",
description: "",
members: new Set([]),
is_system_group: false,
};
const admins_group_item = user_group_item(admins_group);
user_groups.initialize({
realm_user_groups: [bob_system_group, bob_group, second_bob_group, admins_group],
});
function test(label, f) {
run_test(label, (helpers) => {
pm_conversations.clear_for_testing();
@ -942,3 +986,100 @@ test("compare_language", () => {
test("compare_by_pms", () => {
assert.equal(th.compare_by_pms(a_user, a_user), 0);
});
test("sort_group_setting_options", ({override_rewire}) => {
function get_group_setting_typeahead_result(query, target_group) {
const users = people.get_realm_active_human_users().map((user) => ({type: "user", user}));
const groups = user_groups.get_all_realm_user_groups().map((group) => ({
type: "user_group",
...group,
}));
const result = th.sort_group_setting_options({
users,
query,
groups,
target_group,
});
return result.map((item) => {
if (item.type === "user") {
return item.user.full_name;
}
return item.name;
});
}
assert.deepEqual(get_group_setting_typeahead_result("Bo", second_bob_group), [
bob_system_group.name,
bob_group.name,
second_bob_group.name,
b_user_2.full_name,
b_user_1.full_name,
b_user_3.full_name,
admins_group.name,
a_user.full_name,
zman.full_name,
]);
assert.deepEqual(get_group_setting_typeahead_result("bo", second_bob_group), [
bob_system_group.name,
bob_group.name,
second_bob_group.name,
b_user_2.full_name,
b_user_1.full_name,
b_user_3.full_name,
admins_group.name,
a_user.full_name,
zman.full_name,
]);
assert.deepEqual(get_group_setting_typeahead_result("Z", second_bob_group), [
zman.full_name,
admins_group.name,
a_user.full_name,
bob_system_group.name,
bob_group.name,
second_bob_group.name,
b_user_2.full_name,
b_user_1.full_name,
b_user_3.full_name,
]);
override_rewire(th, "MAX_ITEMS", 6);
assert.deepEqual(get_group_setting_typeahead_result("Bo", second_bob_group), [
bob_system_group.name,
bob_group.name,
second_bob_group.name,
b_user_2.full_name,
b_user_1.full_name,
b_user_3.full_name,
]);
});
test("compare_setting_options", () => {
// User group has higher priority than user.
assert.equal(th.compare_setting_options(a_user_item, bob_group_item, bob_group), 1);
assert.equal(th.compare_setting_options(bob_group_item, a_user_item, bob_group), -1);
// System user group has higher priority than other user groups.
assert.equal(th.compare_setting_options(bob_group_item, bob_system_group_item, bob_group), 1);
assert.equal(th.compare_setting_options(bob_system_group_item, bob_group_item, bob_group), -1);
assert.equal(
th.compare_setting_options(admins_group_item, bob_system_group_item, bob_group),
1,
);
// In case both groups are not system groups, alphabetical order is used to decide priority.
assert.equal(th.compare_setting_options(bob_group_item, admins_group_item, bob_group), 1);
assert.equal(th.compare_setting_options(admins_group_item, bob_group_item, bob_group), -1);
// A user who is a member of the group being changed has higher priority.
// If both the users are not members of the group being changed, alphabetical order
// is used to decide priority.
assert.equal(th.compare_setting_options(b_user_1_item, b_user_2_item, bob_group), -1);
assert.equal(th.compare_setting_options(b_user_1_item, b_user_2_item, second_bob_group), 1);
// Get coverage for case where two users have same names. Original order is preserved
// in such cases.
assert.equal(th.compare_setting_options(b_user_1_item, b_user_1_item, bob_group), 0);
});