settings: Remove "User groups" panel from settings overlay.

The "User groups" panel is now removed from settings overlay
and we instead use new "#groups" UI.

This commit also makes some changes to tests to ensure coverage
for pill_typeahead.js which was previously done by
settings_user_group_legacy.test.js. We have still not got
complete coverage on user_pill.ts as we have removed
settings_user_group_legacy.test.js, but we just add the file
to EXEMPT_FILS list for now and will handle it in future.

Fixes #28012.
This commit is contained in:
Sahil Batra 2023-12-06 15:12:25 +05:30 committed by Tim Abbott
parent 17d25284a2
commit 3d181a8ee1
15 changed files with 14 additions and 1448 deletions

View File

@ -215,7 +215,6 @@ EXEMPT_FILES = make_set(
"web/src/settings_streams.js",
"web/src/settings_toggle.js",
"web/src/settings_ui.ts",
"web/src/settings_user_groups_legacy.js",
"web/src/settings_user_topics.js",
"web/src/settings_users.js",
"web/src/setup.ts",
@ -269,6 +268,7 @@ EXEMPT_FILES = make_set(
"web/src/user_group_popover.js",
"web/src/user_group_ui_updates.js",
"web/src/user_groups.ts",
"web/src/user_pill.ts",
"web/src/user_profile.js",
"web/src/user_settings.ts",
"web/src/user_sort.ts",

View File

@ -77,7 +77,6 @@ function insert_tip_box() {
$(".organization-box")
.find(".settings-section")
.not("#emoji-settings")
.not("#user-groups-admin")
.not("#organization-auth-settings")
.not("#admin-bot-list")
.not("#admin-invites-list")
@ -116,7 +115,6 @@ export function build_page() {
realm_inline_url_embed_preview: page_params.realm_inline_url_embed_preview,
server_inline_url_embed_preview: page_params.server_inline_url_embed_preview,
realm_authentication_methods: page_params.realm_authentication_methods,
realm_user_group_edit_policy: page_params.realm_user_group_edit_policy,
realm_name_changes_disabled: page_params.realm_name_changes_disabled,
realm_email_changes_disabled: page_params.realm_email_changes_disabled,
realm_avatar_changes_disabled: page_params.realm_avatar_changes_disabled,
@ -174,7 +172,6 @@ export function build_page() {
can_create_multiuse_invite: settings_data.user_can_create_multiuse_invite(),
can_invite_users_by_email: settings_data.user_can_invite_users_by_email(),
realm_invite_required: page_params.realm_invite_required,
can_edit_user_groups: settings_data.user_can_edit_user_groups(),
policy_values: settings_config.common_policy_values,
realm_delete_own_message_policy: page_params.realm_delete_own_message_policy,
DELETE_OWN_MESSAGE_POLICY_ADMINS_ONLY:

View File

@ -62,7 +62,6 @@ import * as settings_profile_fields from "./settings_profile_fields";
import * as settings_realm_domains from "./settings_realm_domains";
import * as settings_realm_user_settings_defaults from "./settings_realm_user_settings_defaults";
import * as settings_streams from "./settings_streams";
import * as settings_user_groups_legacy from "./settings_user_groups_legacy";
import * as settings_users from "./settings_users";
import * as sidebar_ui from "./sidebar_ui";
import * as starred_messages from "./starred_messages";
@ -882,7 +881,6 @@ export function dispatch_normal_event(event) {
blueslip.error("Unexpected event type user_group/" + event.op);
break;
}
settings_user_groups_legacy.reload();
break;
case "user_status":

View File

@ -15,7 +15,6 @@ import * as settings_playgrounds from "./settings_playgrounds";
import * as settings_profile_fields from "./settings_profile_fields";
import * as settings_realm_user_settings_defaults from "./settings_realm_user_settings_defaults";
import * as settings_streams from "./settings_streams";
import * as settings_user_groups_legacy from "./settings_user_groups_legacy";
import * as settings_user_topics from "./settings_user_topics";
import * as settings_users from "./settings_users";
@ -72,7 +71,6 @@ export function initialize() {
load_func_dict.set("linkifier-settings", settings_linkifiers.set_up);
load_func_dict.set("playground-settings", settings_playgrounds.set_up);
load_func_dict.set("invites-list-admin", settings_invites.set_up);
load_func_dict.set("user-groups-admin", settings_user_groups_legacy.set_up);
load_func_dict.set("profile-field-settings", settings_profile_fields.set_up);
load_func_dict.set("data-exports-admin", settings_exports.set_up);
load_func_dict.set(
@ -112,7 +110,6 @@ export function reset_sections() {
settings_org.reset();
settings_profile_fields.reset();
settings_streams.reset();
settings_user_groups_legacy.reset();
settings_user_topics.reset();
settings_muted_users.reset();
alert_words_ui.reset();

View File

@ -1,407 +0,0 @@
import $ from "jquery";
import _ from "lodash";
import render_confirm_delete_user from "../templates/confirm_dialog/confirm_delete_user.hbs";
import render_add_user_group_modal from "../templates/settings/add_user_group_modal.hbs";
import render_admin_user_group_list from "../templates/settings/admin_user_group_list.hbs";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import * as dialog_widget from "./dialog_widget";
import {$t, $t_html} from "./i18n";
import * as keydown_util from "./keydown_util";
import {page_params} from "./page_params";
import * as people from "./people";
import * as pill_typeahead from "./pill_typeahead";
import * as settings_data from "./settings_data";
import * as ui_report from "./ui_report";
import * as user_groups from "./user_groups";
import * as user_pill from "./user_pill";
const meta = {
loaded: false,
};
export function reset() {
meta.loaded = false;
}
export function reload() {
if (!meta.loaded) {
return;
}
const $user_groups_section = $("#user-groups").expectOne();
$user_groups_section.empty();
populate_user_groups();
}
export function can_edit(group_id) {
if (!settings_data.user_can_edit_user_groups()) {
return false;
}
// Admins and moderators are allowed to edit user groups even if they
// are not a member of that user group. Members can edit user groups
// only if they belong to that group.
if (page_params.is_admin || page_params.is_moderator) {
return true;
}
return user_groups.is_direct_member_of(people.my_current_user_id(), group_id);
}
export function populate_user_groups() {
const $user_groups_section = $("#user-groups").expectOne();
const user_groups_array = user_groups.get_realm_user_groups();
for (const data of user_groups_array) {
$user_groups_section.append(
render_admin_user_group_list({
user_group: {
name: data.name,
id: data.id,
description: data.description,
},
}),
);
const pill_config = {
show_user_status_emoji: false,
};
const $pill_container = $(`.pill-container[data-group-pills="${CSS.escape(data.id)}"]`);
const pills = user_pill.create_pills($pill_container, pill_config);
function get_pill_user_ids() {
return user_pill.get_user_ids(pills);
}
const $userg = $(`div.user-group[id="${CSS.escape(data.id)}"]`);
for (const user_id of data.members) {
const user = people.get_by_user_id(user_id);
user_pill.append_user(user, pills);
}
function update_membership(group_id) {
if (can_edit(group_id)) {
return;
}
$userg.find(".name").attr("contenteditable", "false");
$userg.find(".description").attr("contenteditable", "false");
$userg.addClass("ntm");
$pill_container.find(".input").attr("contenteditable", "false");
$pill_container.find(".input").css("display", "none");
$pill_container.addClass("not-editable");
$pill_container.off("keydown", ".pill");
$pill_container.off("keydown", ".input");
$pill_container.off("click");
$pill_container.on("click", (e) => {
e.stopPropagation();
});
$pill_container.find(".pill").on("mouseenter", () => {
$pill_container.find(".pill").find(".exit").css("opacity", "0.5");
});
}
update_membership(data.id);
function is_user_group_changed() {
const draft_group = get_pill_user_ids();
const group_data = user_groups.get_user_group_from_id(data.id);
const original_group = [...group_data.members];
const same_groups = _.isEqual(_.sortBy(draft_group), _.sortBy(original_group));
const description = $(`#user-groups #${CSS.escape(data.id)} .description`)
.text()
.trim();
const name = $(`#user-groups #${CSS.escape(data.id)} .name`)
.text()
.trim();
const $user_group_status = $(`#user-groups #${CSS.escape(data.id)} .user-group-status`);
if ($user_group_status.is(":visible")) {
return false;
}
if (
group_data.description === description &&
group_data.name === name &&
(!draft_group.length || same_groups)
) {
return false;
}
return true;
}
function update_cancel_button() {
if (!can_edit(data.id)) {
return;
}
const $cancel_button = $(
`#user-groups #${CSS.escape(data.id)} .save-status.btn-danger`,
);
const $saved_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.sea-green`);
const $save_instructions = $(`#user-groups #${CSS.escape(data.id)} .save-instructions`);
if (is_user_group_changed() && !$cancel_button.is(":visible")) {
$saved_button.fadeOut(0);
$cancel_button.css({display: "inline-block", opacity: "0"}).fadeTo(400, 1);
$save_instructions.css({display: "block", opacity: "0"}).fadeTo(400, 1);
} else if (!is_user_group_changed() && $cancel_button.is(":visible")) {
$cancel_button.fadeOut();
$save_instructions.fadeOut();
}
}
function show_saved_button() {
const $cancel_button = $(
`#user-groups #${CSS.escape(data.id)} .save-status.btn-danger`,
);
const $saved_button = $(`#user-groups #${CSS.escape(data.id)} .save-status.sea-green`);
const $save_instructions = $(`#user-groups #${CSS.escape(data.id)} .save-instructions`);
if (!$saved_button.is(":visible")) {
$cancel_button.fadeOut(0);
$save_instructions.fadeOut(0);
$saved_button
.css({display: "inline-block", opacity: "0"})
.fadeTo(400, 1)
.delay(2000)
.fadeTo(400, 0);
}
}
function save_members() {
const draft_group = get_pill_user_ids();
const group_data = user_groups.get_user_group_from_id(data.id);
const original_group = [...group_data.members];
const same_groups = _.isEqual(_.sortBy(draft_group), _.sortBy(original_group));
if (!draft_group.length || same_groups) {
return;
}
const added = _.difference(draft_group, original_group);
const removed = _.difference(original_group, draft_group);
channel.post({
url: "/json/user_groups/" + data.id + "/members",
data: {
add: JSON.stringify(added),
delete: JSON.stringify(removed),
},
success() {
setTimeout(show_saved_button, 200);
},
});
}
function save_name_desc() {
const $user_group_status = $(`#user-groups #${CSS.escape(data.id)} .user-group-status`);
const group_data = user_groups.get_user_group_from_id(data.id);
const description = $(`#user-groups #${CSS.escape(data.id)} .description`)
.text()
.trim();
const name = $(`#user-groups #${CSS.escape(data.id)} .name`)
.text()
.trim();
if (group_data.description === description && group_data.name === name) {
return;
}
channel.patch({
url: "/json/user_groups/" + data.id,
data: {
name,
description,
},
success() {
$user_group_status.hide();
setTimeout(show_saved_button, 200);
},
error(xhr) {
ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $user_group_status);
update_cancel_button();
$(`#user-groups #${CSS.escape(data.id)} .name`).text(group_data.name);
$(`#user-groups #${CSS.escape(data.id)} .description`).text(
group_data.description,
);
},
});
}
function do_not_blur(except_class, event) {
// Event generated from or inside the typeahead.
if ($(event.relatedTarget).closest(".typeahead").length) {
return true;
}
if ($(event.relatedTarget).closest(`#user-groups #${CSS.escape(data.id)}`).length) {
return [".pill-container", ".name", ".description", ".input", ".delete"].some(
(class_name) =>
class_name !== except_class &&
$(event.relatedTarget).closest(class_name).length,
);
}
return false;
}
function auto_save(class_name, event) {
if (!can_edit(data.id)) {
return;
}
if (do_not_blur(class_name, event)) {
return;
}
if (
$(event.relatedTarget).closest(`#user-groups #${CSS.escape(data.id)}`) &&
$(event.relatedTarget).closest(".save-status.btn-danger").length
) {
reload();
return;
}
save_name_desc();
save_members();
}
$(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".input", (event) => {
auto_save(".input", event);
});
$(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".name", (event) => {
auto_save(".name", event);
});
$(`#user-groups #${CSS.escape(data.id)}`).on("input", ".name", () => {
update_cancel_button();
});
$(`#user-groups #${CSS.escape(data.id)}`).on("blur", ".description", (event) => {
auto_save(".description", event);
});
$(`#user-groups #${CSS.escape(data.id)}`).on("input", ".description", () => {
update_cancel_button();
});
const $input = $pill_container.children(".input");
if (can_edit(data.id)) {
const opts = {update_func: update_cancel_button, user: true};
pill_typeahead.set_up($input, pills, opts);
}
if (can_edit(data.id)) {
pills.onPillRemove(() => {
// onPillRemove is fired before the pill is removed from
// the DOM.
update_cancel_button();
setTimeout(() => {
$input.trigger("focus");
}, 100);
});
}
}
}
export function add_user_group() {
const $user_group_status = $("#dialog_error");
const group = {
members: JSON.stringify([people.my_current_user_id()]),
};
for (const obj of $("#add-user-group-form").serializeArray()) {
if (obj.value.trim() === "") {
continue;
}
group[obj.name] = obj.value;
}
channel.post({
url: "/json/user_groups/create",
data: group,
success() {
$user_group_status.hide();
ui_report.success($t_html({defaultMessage: "User group added!"}), $user_group_status);
dialog_widget.close();
},
error(xhr) {
$user_group_status.hide();
ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $user_group_status);
},
});
}
function show_add_user_group_modal() {
const html_body = render_add_user_group_modal();
function add_user_group_post_render() {
const $add_user_group_input_element = $("#user_group_name");
const $add_user_group_submit_button = $("#add-user-group-modal .dialog_submit_button");
$add_user_group_submit_button.prop("disabled", true);
$add_user_group_input_element.on("input", () => {
$add_user_group_submit_button.prop(
"disabled",
$add_user_group_input_element.val() === "",
);
});
}
dialog_widget.launch({
html_heading: $t_html({defaultMessage: "Add new user group"}),
html_body,
html_submit_button: $t_html({defaultMessage: "Save"}),
help_link: "/help/user-groups",
form_id: "add-user-group-form",
id: "add-user-group-modal",
on_click: add_user_group,
on_shown: () => $("#user_group_name").trigger("focus"),
post_render: add_user_group_post_render,
});
}
export function set_up() {
meta.loaded = true;
populate_user_groups();
$("#show-add-user-group-modal").on("click", (e) => {
e.preventDefault();
e.stopPropagation();
show_add_user_group_modal();
});
$("#user-groups").on("click", ".delete", function () {
const group_id = Number.parseInt($(this).parents(".user-group").attr("id"), 10);
if (!can_edit(group_id)) {
return;
}
const user_group = user_groups.get_user_group_from_id(group_id);
const $btn = $(this);
function delete_user_group() {
channel.del({
url: "/json/user_groups/" + group_id,
data: {
id: group_id,
},
error() {
$btn.text($t({defaultMessage: "Failed!"}));
},
});
}
const html_body = render_confirm_delete_user({
group_name: user_group.name,
});
const user_group_name = user_group.name;
confirm_dialog.launch({
html_heading: $t_html({defaultMessage: "Delete {user_group_name}?"}, {user_group_name}),
html_body,
on_click: delete_user_group,
});
});
$("#user-groups").on("keypress", ".user-group h4 > span", (e) => {
if (keydown_util.is_enter_event(e)) {
e.preventDefault();
}
});
}

View File

@ -651,7 +651,6 @@
.clear_search_button:focus,
.clear_search_button:active,
.clear_search_button:disabled:hover,
#user-groups .save-instructions,
.close {
color: hsl(236deg 33% 80%);
}

View File

@ -687,19 +687,6 @@ input[type="checkbox"] {
text-align: right;
}
#show-add-user-group-modal {
margin-bottom: 10px;
}
#add-user-group-form {
margin: 0;
/* This 14px is the border and padding of the input element */
#user_group_description {
width: calc(100% - 14px);
}
}
.add-new-export-box {
margin: 10px 0;
}
@ -1220,102 +1207,6 @@ $option_title_width: 180px;
margin-right: 5px;
}
#user-groups {
.user-group {
margin-bottom: 20px;
padding: 10px;
border-radius: 5px;
& h4 {
font-weight: normal;
margin: 0;
display: flex;
align-items: center;
justify-content: left;
}
& span[contenteditable] {
display: inline-block;
word-break: break-all;
&:empty::before {
opacity: 0.5;
display: inline-block;
content: attr(data-placeholder);
}
}
& span[contenteditable]:focus,
span[contenteditable="true"]:hover {
border-bottom: 1px solid hsl(0deg 0% 80%);
margin-bottom: -1px;
outline: none;
}
.pill-container .input[contenteditable]:empty::after {
content: attr(data-placeholder);
opacity: 0.5;
}
}
.user-group-status {
margin-bottom: 10px;
}
& p {
line-height: 2;
margin: 0;
}
.spacer {
margin: 0 2px;
}
.subscribers,
.user-group h4 > .name {
font-weight: bold;
}
.ntm {
cursor: not-allowed;
& h4 > .button {
cursor: not-allowed;
display: none;
&:hover {
border-color: hsl(4deg 56% 82%);
}
}
}
.save-status {
background-color: transparent;
padding: 2px 5px;
border-radius: 4px;
margin-left: 10px;
border-style: solid;
border-width: 1px;
display: none;
opacity: 0;
}
.checkmark {
height: 12px;
}
.delete {
margin-left: auto;
}
.save-instructions {
display: none;
opacity: 0;
color: hsl(0deg 0% 20%);
font-size: 0.9em;
}
}
/* -- new settings overlay -- */
#settings_page {
height: 95vh;

View File

@ -29,8 +29,6 @@
{{> invites_list_admin }}
{{> user_groups_admin }}
{{> profile_field_settings_admin }}
{{> data_exports_admin }}

View File

@ -1,27 +0,0 @@
{{#with user_group}}
<div class="user-group white-box" id="{{id}}">
<div class="alert user-group-status"></div>
<h4>
<span class="name" data-placeholder="{{t 'Name' }}" contenteditable="true" spellcheck="false">{{name}}</span>
<span class="spacer">—</span>
<span class="description" data-placeholder="{{t 'Description' }}" contenteditable="true">{{description}}</span>
<button class="button save-status sea-green small">
<img class="checkmark" src="../../images/checkbox-green.svg" />
{{t 'Saved' }}
</button>
<button class="button save-status btn-danger small">
<i class="fa fa-undo" aria-label="{{t 'Delete' }}" title="{{t 'Delete' }}"></i>
</button>
<button class="button rounded small delete btn-danger">
<i class="fa fa-trash-o" aria-label="{{t 'Delete' }}" title="{{t 'Delete' }}"></i>
</button>
</h4>
<p class="subscribers">{{t 'Subscribers' }}</p>
<div class="pill-container" data-group-pills="{{id}}">
<div class="input" contenteditable="true" data-placeholder="{{t 'Add member…' }}"></div>
</div>
<p class="save-instructions">
{{t "Click outside the input box to save. We'll automatically notify anyone that was added or removed."}}
</p>
</div>
{{/with}}

View File

@ -1,28 +0,0 @@
<div id="user-groups-admin" class="settings-section" data-name="user-groups-admin">
<div class="user-group-setting-tip-container {{#unless (or is_admin (not can_edit_user_groups))}}hide{{/unless}}">
{{#if (eq realm_user_group_edit_policy policy_values.by_members.code) }}
<div class="tip">{{t 'This organization is configured so that administrators, moderators and group members can modify user groups.' }}</div>
{{else if (eq realm_user_group_edit_policy policy_values.by_full_members.code) }}
<div class="tip">{{t 'This organization is configured so that administrators, moderators and full members belonging to the group can modify user groups.' }}</div>
{{else if (eq realm_user_group_edit_policy policy_values.by_moderators_only.code) }}
<div class="tip">{{t 'This organization is configured so that administrators and moderators can modify user groups.' }}</div>
{{else}}
<div class="tip">{{t 'This organization is configured so that only administrators can modify user groups.' }}</div>
{{/if}}
</div>
{{#unless is_guest}}
<p>
{{#tr}}
User groups allow you to <z-link>mention</z-link> multiple users at once. When you mention a user group, everyone in the group is notified as if they were individually mentioned.
{{#*inline "z-link"}}<a href="/help/mention-a-user-or-group" target="_blank" rel="noopener noreferrer">{{> @partial-block}}</a>{{/inline}}
{{/tr}}
</p>
{{#if can_edit_user_groups}}
<button id="show-add-user-group-modal" class="button rounded sea-green">
{{t 'Add a new user group' }}
</button>
{{/if}}
{{/unless}}
<div id="user-groups" class="new-style"></div>
</div>

View File

@ -86,12 +86,6 @@
<i class="locked fa fa-lock tippy-zulip-tooltip" {{#if is_admin}}style="display: none;"{{/if}} data-tippy-content="{{t 'Only organization administrators can edit these settings.' }}"></i>
</li>
{{#unless is_guest}}
<li tabindex="0" data-section="user-groups-admin">
<i class="icon fa fa-group" aria-hidden="true"></i>
<div class="text">{{t "User groups" }}</div>
</li>
{{/unless}}
{{#unless is_guest}}
<li tabindex="0" data-section="user-list-admin">
<i class="icon fa fa-user" aria-hidden="true"></i>
<div class="text">{{t "Users" }}</div>

View File

@ -63,7 +63,6 @@ const settings_realm_user_settings_defaults = mock_esm(
);
const settings_realm_domains = mock_esm("../src/settings_realm_domains");
const settings_streams = mock_esm("../src/settings_streams");
const settings_user_groups_legacy = mock_esm("../src/settings_user_groups_legacy");
const settings_users = mock_esm("../src/settings_users");
const sidebar_ui = mock_esm("../src/sidebar_ui");
const stream_data = mock_esm("../src/stream_data");
@ -171,7 +170,6 @@ run_test("attachments", ({override}) => {
run_test("user groups", ({override}) => {
let event = event_fixtures.user_group__add;
override(settings_user_groups_legacy, "reload", noop);
{
const stub = make_stub();
const user_group_settings_ui_stub = make_stub();
@ -1165,7 +1163,7 @@ run_test("realm_export", ({override}) => {
assert.equal(args.exports, event.exports);
});
run_test("server_event_dispatch_op_errors", ({override}) => {
run_test("server_event_dispatch_op_errors", () => {
blueslip.expect("error", "Unexpected event type subscription/other");
server_events_dispatch.dispatch_normal_event({type: "subscription", op: "other"});
blueslip.expect("error", "Unexpected event type reaction/other");
@ -1190,7 +1188,6 @@ run_test("server_event_dispatch_op_errors", ({override}) => {
sender: {user_id: 5},
op: "other",
});
override(settings_user_groups_legacy, "reload", noop);
blueslip.expect("error", "Unexpected event type user_group/other");
server_events_dispatch.dispatch_normal_event({type: "user_group", op: "other"});
});

View File

@ -124,6 +124,11 @@ run_test("set_up", ({mock_template}) => {
get_text_from_item: noop,
});
let update_func_called = false;
function update_func() {
update_func_called = true;
}
let opts = {};
$fake_input.typeahead = (config) => {
assert.equal(config.items, 5);
@ -297,6 +302,8 @@ run_test("set_up", ({mock_template}) => {
assert.equal(number_of_pills(), 2);
config.updater.call(fake_group_this, testers);
assert.equal(number_of_pills(), 3);
assert.ok(update_func_called);
}
})();
@ -324,7 +331,7 @@ run_test("set_up", ({mock_template}) => {
{user_group: true, stream: true},
{user_group: true, user: true},
{user: true, stream: true},
{user_group: true, stream: true, user: true},
{user_group: true, stream: true, user: true, update_func},
];
for (const config of all_possible_opts) {

View File

@ -1,854 +0,0 @@
"use strict";
const {strict: assert} = require("assert");
const {$t} = require("./lib/i18n");
const {mock_esm, set_global, zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const blueslip = require("./lib/zblueslip");
const $ = require("./lib/zjquery");
const {page_params} = require("./lib/zpage_params");
const noop = () => {};
const pills = {
pill: {},
};
let create_item_handler;
const channel = mock_esm("../src/channel");
const confirm_dialog = mock_esm("../src/confirm_dialog");
const dialog_widget = mock_esm("../src/dialog_widget");
const input_pill = mock_esm("../src/input_pill");
const settings_data = mock_esm("../src/settings_data");
const typeahead_helper = mock_esm("../src/typeahead_helper");
const user_groups = mock_esm("../src/user_groups", {
get_user_group_from_id: noop,
remove: noop,
add: noop,
});
const ui_report = mock_esm("../src/ui_report");
const people = zrequire("people");
const settings_user_groups_legacy = zrequire("settings_user_groups_legacy");
const user_pill = zrequire("user_pill");
function reset_test_setup($pill_container_stub) {
function input_pill_stub(opts) {
assert.equal(opts.$container, $pill_container_stub);
create_item_handler = opts.create_item_from_text;
assert.ok(create_item_handler);
return pills;
}
input_pill.create = input_pill_stub;
}
function test_ui(label, f) {
// The sloppy_$ flag lets us reuse setup from prior tests.
run_test(label, f, {sloppy_$: true});
}
test_ui("can_edit", ({override}) => {
override(settings_data, "user_can_edit_user_groups", () => false);
assert.ok(!settings_user_groups_legacy.can_edit(1));
override(settings_data, "user_can_edit_user_groups", () => true);
user_groups.is_direct_member_of = (user_id, group_id) => {
assert.equal(group_id, 1);
assert.equal(user_id, undefined);
return false;
};
assert.ok(!settings_user_groups_legacy.can_edit(1));
page_params.is_admin = true;
assert.ok(settings_user_groups_legacy.can_edit(1));
page_params.is_admin = false;
page_params.is_moderator = true;
assert.ok(settings_user_groups_legacy.can_edit(1));
page_params.is_admin = false;
page_params.is_moderator = false;
user_groups.is_direct_member_of = (user_id, group_id) => {
assert.equal(group_id, 1);
assert.equal(user_id, undefined);
return true;
};
assert.ok(settings_user_groups_legacy.can_edit(1));
});
const user_group_selector = `#user-groups #${CSS.escape(1)}`;
const cancel_selector = `#user-groups #${CSS.escape(1)} .save-status.btn-danger`;
const saved_selector = `#user-groups #${CSS.escape(1)} .save-status.sea-green`;
const name_selector = `#user-groups #${CSS.escape(1)} .name`;
const description_selector = `#user-groups #${CSS.escape(1)} .description`;
const instructions_selector = `#user-groups #${CSS.escape(1)} .save-instructions`;
test_ui("populate_user_groups", ({mock_template, override, override_rewire}) => {
override(settings_data, "user_can_edit_user_groups", () => true);
const realm_user_group = {
id: 1,
name: "Mobile",
description: "All mobile people",
members: new Set([2, 4]),
};
const iago = {
email: "iago@zulip.com",
user_id: 2,
full_name: "Iago",
};
const alice = {
email: "alice@example.com",
user_id: 31,
full_name: "Alice",
};
const bob = {
email: "bob@example.com",
user_id: 32,
full_name: "Bob",
};
people.add_active_user(iago);
people.add_active_user(alice);
people.add_active_user(bob);
override_rewire(people, "get_realm_users", () => [iago, alice, bob]);
user_groups.get_realm_user_groups = () => [realm_user_group];
let templates_render_called = false;
const $fake_rendered_temp = $.create("fake_admin_user_group_list_template_rendered");
mock_template("settings/admin_user_group_list.hbs", false, (args) => {
assert.equal(args.user_group.id, 1);
assert.equal(args.user_group.name, "Mobile");
assert.equal(args.user_group.description, "All mobile people");
templates_render_called = true;
return $fake_rendered_temp;
});
let user_groups_list_append_called = false;
$("#user-groups").append = (rendered_temp) => {
assert.equal(rendered_temp, $fake_rendered_temp);
user_groups_list_append_called = true;
};
let get_by_user_id_called = false;
override_rewire(people, "get_by_user_id", (user_id) => {
if (user_id === iago.user_id) {
return iago;
}
assert.equal(user_id, 4);
blueslip.expect("warn", "Undefined user in function append_user");
get_by_user_id_called = true;
return undefined;
});
override_rewire(
people,
"is_known_user",
() => people.get_by_user_id !== undefined && people.get_by_user_id !== noop,
);
page_params.is_admin = true;
const all_pills = new Map();
const $pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
pills.appendValidatedData = (item) => {
const id = item.user_id;
assert.ok(!all_pills.has(id));
all_pills.set(id, item);
};
pills.items = () => [...all_pills.values()];
let text_cleared;
pills.clear_text = () => {
text_cleared = true;
};
const $input_field_stub = $.create("fake-input-field");
$pill_container_stub.children = () => $input_field_stub;
let input_typeahead_called = false;
$input_field_stub.typeahead = (config) => {
assert.equal(config.items, 5);
assert.ok(config.fixed);
assert.ok(config.dropup);
assert.ok(config.stopAdvance);
assert.equal(typeof config.source, "function");
assert.equal(typeof config.highlighter, "function");
assert.equal(typeof config.matcher, "function");
assert.equal(typeof config.sorter, "function");
assert.equal(typeof config.updater, "function");
(function test_highlighter() {
const $fake_person = $.create("fake-person");
typeahead_helper.render_person = () => $fake_person;
assert.equal(config.highlighter(), $fake_person);
})();
const fake_context = {
query: "ali",
};
const fake_context_for_email = {
query: "am",
};
(function test_source() {
const result = config.source.call(fake_context, iago);
const emails = result.map((user) => user.email).sort();
assert.deepEqual(emails, [alice.email, bob.email]);
})();
(function test_matcher() {
/* Here the query doesn't begin with an '@' because typeahead is triggered
by the '@' sign and thus removed in the query. */
let result = config.matcher.call(fake_context, iago);
assert.ok(!result);
result = config.matcher.call(fake_context, alice);
assert.ok(result);
bob.delivery_email = null;
result = config.matcher.call(fake_context_for_email, bob);
assert.ok(!result);
bob.delivery_email = "bob-delivery@example.com";
result = config.matcher.call(fake_context_for_email, bob);
assert.ok(result);
})();
(function test_sorter() {
let sort_recipients_typeahead_called = false;
typeahead_helper.sort_recipients = function () {
sort_recipients_typeahead_called = true;
};
config.sorter.call(fake_context, []);
assert.ok(sort_recipients_typeahead_called);
})();
(function test_updater() {
$input_field_stub.text("@ali");
user_groups.get_user_group_from_id = () => realm_user_group;
let saved_fade_out_called = false;
let cancel_fade_to_called = false;
let instructions_fade_to_called = false;
$(saved_selector).fadeOut = () => {
saved_fade_out_called = true;
};
$(cancel_selector).css = (data) => {
assert.equal(typeof data, "object");
assert.equal(data.display, "inline-block");
assert.equal(data.opacity, "0");
return $(cancel_selector);
};
$(cancel_selector).fadeTo = () => {
cancel_fade_to_called = true;
};
$(instructions_selector).css = (data) => {
assert.equal(typeof data, "object");
assert.equal(data.display, "block");
assert.equal(data.opacity, "0");
return $(instructions_selector);
};
$(instructions_selector).fadeTo = () => {
instructions_fade_to_called = true;
};
text_cleared = false;
config.updater(alice);
// update_cancel_button is called.
assert.ok(saved_fade_out_called);
assert.ok(cancel_fade_to_called);
assert.ok(instructions_fade_to_called);
assert.equal(text_cleared, true);
})();
input_typeahead_called = true;
};
let get_by_email_called = false;
override_rewire(people, "get_by_email", (user_email) => {
get_by_email_called = true;
switch (user_email) {
case iago.email:
return iago;
case bob.email:
return bob;
/* istanbul ignore next */
default:
throw new Error("Expected user email to be of Iago or Bob here.");
}
});
function test_create_item(handler) {
(function test_rejection_path() {
const item = handler(iago.email, pills.items());
assert.ok(get_by_email_called);
assert.equal(item, undefined);
})();
(function test_success_path() {
get_by_email_called = false;
const res = handler(bob.email, pills.items());
assert.ok(get_by_email_called);
assert.equal(typeof res, "object");
assert.equal(res.user_id, bob.user_id);
assert.equal(res.display_value, bob.full_name);
})();
(function test_deactivated_pill() {
people.deactivate(bob);
get_by_email_called = false;
const res = handler(bob.email, pills.items());
assert.ok(get_by_email_called);
assert.equal(typeof res, "object");
assert.equal(res.user_id, bob.user_id);
assert.equal(res.display_value, bob.full_name);
assert.ok(res.deactivated);
people.add_active_user(bob);
})();
}
pills.onPillRemove = (handler) => {
set_global("setTimeout", (func) => {
func();
});
realm_user_group.members = new Set([2, 31]);
handler();
};
reset_test_setup($pill_container_stub);
settings_user_groups_legacy.set_up();
assert.ok(templates_render_called);
assert.ok(user_groups_list_append_called);
assert.ok(get_by_user_id_called);
assert.ok(input_typeahead_called);
test_create_item(create_item_handler);
// Tests for settings_user_groups_legacy.set_up workflow.
assert.equal(typeof $("#user-groups").get_on_handler("click", ".delete"), "function");
assert.equal(
typeof $("#user-groups").get_on_handler("keypress", ".user-group h4 > span"),
"function",
);
});
test_ui("with_external_user", ({disallow_rewire, override_rewire, mock_template}) => {
const realm_user_group = {
id: 1,
name: "Mobile",
description: "All mobile people",
members: new Set([2, 4]),
};
user_groups.get_realm_user_groups = () => [realm_user_group];
// These are already tested, so we skip them
disallow_rewire(people, "get_realm_users");
mock_template(
"settings/admin_user_group_list.hbs",
false,
() => "settings/admin_user_group_list.hbs",
);
override_rewire(people, "get_by_user_id", () => "user stub");
override_rewire(user_pill, "append_person", noop);
let can_edit_called = 0;
override_rewire(settings_user_groups_legacy, "can_edit", () => {
can_edit_called += 1;
return false;
});
// Reset zjquery to test stuff with user who cannot edit
$.clear_all_elements();
let user_group_find_called = 0;
const $user_group_stub = $(`div.user-group[id="${CSS.escape(1)}"]`);
const $name_field_stub = $.create("fake-name-field");
const $description_field_stub = $.create("fake-description-field");
const $input_stub = $.create("fake-input");
$user_group_stub.find = (elem) => {
user_group_find_called += 1;
switch (elem) {
case ".name":
return $name_field_stub;
case ".description":
return $description_field_stub;
/* istanbul ignore next */
default:
throw new Error(`Unknown element ${elem}`);
}
};
const $pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
const $pill_stub = $.create("fake-pill");
let pill_container_find_called = 0;
$pill_container_stub.find = (elem) => {
pill_container_find_called += 1;
switch (elem) {
case ".input":
return $input_stub;
case ".pill":
return $pill_stub;
/* istanbul ignore next */
default:
throw new Error(`Unknown element ${elem}`);
}
};
$input_stub.css = (property, val) => {
assert.equal(property, "display");
assert.equal(val, "none");
};
// Test the 'off' handlers on the pill-container
const turned_off = {};
$pill_container_stub.off = (event_name, sel = "whole") => {
turned_off[event_name + "/" + sel] = true;
};
const $exit_button = $.create("fake-pill-exit");
$pill_stub.set_find_results(".exit", $exit_button);
let exit_button_called = false;
$exit_button.css = (property, value) => {
exit_button_called = true;
assert.equal(property, "opacity");
assert.equal(value, "0.5");
};
// We return [] because these are already tested, so we skip them
$pill_container_stub.children = () => [];
$("#user-groups").append = noop;
reset_test_setup($pill_container_stub);
settings_user_groups_legacy.set_up();
let set_parents_result_called = 0;
let set_attributes_called = 0;
// Test different handlers with an external user
const delete_handler = $("#user-groups").get_on_handler("click", ".delete");
const $fake_delete = $.create("fk-#user-groups.delete_btn");
$fake_delete.set_parents_result(".user-group", $(".user-group"));
set_parents_result_called += 1;
$(".user-group").attr("id", "1");
set_attributes_called += 1;
const name_update_handler = $(user_group_selector).get_on_handler("input", ".name");
const des_update_handler = $(user_group_selector).get_on_handler("input", ".description");
const member_change_handler = $(user_group_selector).get_on_handler("blur", ".input");
const name_change_handler = $(user_group_selector).get_on_handler("blur", ".name");
const des_change_handler = $(user_group_selector).get_on_handler("blur", ".description");
const event = {
stopPropagation: noop,
};
const pill_mouseenter_handler = $pill_stub.get_on_handler("mouseenter");
const pill_click_handler = $pill_container_stub.get_on_handler("click");
pill_mouseenter_handler(event);
pill_click_handler(event);
assert.equal(delete_handler.call($fake_delete), undefined);
assert.equal(name_update_handler(), undefined);
assert.equal(des_update_handler(), undefined);
assert.equal(member_change_handler(), undefined);
assert.equal(name_change_handler(), undefined);
assert.equal(des_change_handler(), undefined);
assert.equal(set_parents_result_called, 1);
assert.equal(set_attributes_called, 1);
assert.equal(can_edit_called, 9);
assert.ok(exit_button_called);
assert.equal(user_group_find_called, 2);
assert.equal(pill_container_find_called, 4);
assert.equal(turned_off["keydown/.pill"], true);
assert.equal(turned_off["keydown/.input"], true);
assert.equal(turned_off["click/whole"], true);
});
test_ui("reload", ({override_rewire}) => {
$("#user-groups").html("Some text");
let populate_user_groups_called = false;
override_rewire(settings_user_groups_legacy, "populate_user_groups", () => {
populate_user_groups_called = true;
});
settings_user_groups_legacy.reload();
assert.ok(populate_user_groups_called);
assert.equal($("#user-groups").html(), "");
});
test_ui("reset", () => {
settings_user_groups_legacy.reset();
const result = settings_user_groups_legacy.reload();
assert.equal(result, undefined);
});
test_ui("on_events", ({mock_template, override, override_rewire}) => {
override(settings_data, "user_can_edit_user_groups", () => true);
mock_template("confirm_dialog/confirm_delete_user.hbs", false, (data) => {
assert.deepEqual(data, {
group_name: "Mobile",
});
return "stub";
});
page_params.is_admin = true;
(function test_admin_user_group_form_submit_triggered() {
const handler = settings_user_groups_legacy.add_user_group;
const event = {
stopPropagation: noop,
preventDefault: noop,
};
const $fake_this = $.create("#add-user-group-form");
const fake_object_array = [
{
name: "fake-name",
value: "",
},
{
name: "fake-name",
value: "fake-value",
},
];
$fake_this.serializeArray = () => fake_object_array;
channel.post = (opts) => {
const data = {
members: "[null]",
};
data[fake_object_array[1].name] = fake_object_array[1].value;
assert.equal(opts.url, "/json/user_groups/create");
assert.deepEqual(opts.data, data);
(function test_post_success() {
$("#dialog_error").show();
$("#add-user-group-form input[type='text']").val("fake-content");
ui_report.success = (text, ele) => {
assert.equal(text, "translated HTML: User group added!");
assert.equal(ele, $("#dialog_error"));
};
dialog_widget.close = () => {};
opts.success();
assert.ok(!$("#dialog_error").visible());
})();
(function test_post_error() {
$("#dialog_error").show();
ui_report.error = (error_msg, error_obj, ele) => {
assert.equal(error_msg, "translated HTML: Failed");
assert.deepEqual(error_obj, {responseJson: {msg: "fake-msg"}});
assert.equal(ele, $("#dialog_error"));
};
opts.error({responseJson: {msg: "fake-msg"}});
assert.ok(!$("#dialog_error").visible());
})();
};
handler(event);
})();
(function test_user_groups_delete_click_triggered() {
const handler = $("#user-groups").get_on_handler("click", ".delete");
const $fake_this = $.create("fake-#user-groups.delete_btn");
$fake_this.set_parents_result(".user-group", $(".user-group"));
$(".user-group").attr("id", "1");
channel.del = (opts) => {
const data = {
id: 1,
};
assert.equal(opts.url, "/json/user_groups/1");
assert.deepEqual(opts.data, data);
$fake_this.text($t({defaultMessage: "fake-text"}));
opts.error();
assert.equal($fake_this.text(), "translated: Failed!");
};
confirm_dialog.launch = (conf) => {
conf.on_click();
};
handler.call($fake_this);
})();
(function test_user_groups_keypress_enter_triggered() {
const handler = $("#user-groups").get_on_handler("keypress", ".user-group h4 > span");
let default_action_for_enter_stopped = false;
const event = {
key: "Enter",
preventDefault() {
default_action_for_enter_stopped = true;
},
};
handler(event);
assert.ok(default_action_for_enter_stopped);
})();
(function test_do_not_blur() {
const blur_event_classes = [".name", ".description", ".input"];
let api_endpoint_called = false;
/* istanbul ignore next */
channel.post = () => {
api_endpoint_called = true;
};
channel.patch = noop;
const $fake_this = $.create("fake-#user-groups_do_not_blur");
const event = {
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
// Any of the blur_exceptions trigger blur event.
for (const class_name of blur_event_classes) {
const handler = $(user_group_selector).get_on_handler("blur", class_name);
for (const blur_exception of [
".pill-container",
".name",
".description",
".input",
".delete",
]) {
if (blur_exception === class_name) {
continue;
}
api_endpoint_called = false;
$fake_this.closest = (class_name) => {
if (class_name === blur_exception || class_name === user_group_selector) {
return [1];
}
return [];
};
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
}
api_endpoint_called = false;
$fake_this.closest = (class_name) => {
assert.equal(class_name, ".typeahead");
return [1];
};
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
// Cancel button triggers blur event.
let settings_user_groups_legacy_reload_called = false;
override_rewire(settings_user_groups_legacy, "reload", () => {
settings_user_groups_legacy_reload_called = true;
});
api_endpoint_called = false;
$fake_this.closest = (class_name) => {
if (
class_name === ".save-status.btn-danger" ||
class_name === user_group_selector
) {
return [1];
}
return [];
};
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
assert.ok(settings_user_groups_legacy_reload_called);
}
})();
(function test_update_cancel_button() {
const handler_name = $(user_group_selector).get_on_handler("input", ".name");
const handler_desc = $(user_group_selector).get_on_handler("input", ".description");
const $sib_des = $(description_selector);
const $sib_name = $(name_selector);
$sib_name.text($t({defaultMessage: "mobile"}));
$sib_des.text($t({defaultMessage: "All mobile members"}));
const group_data = {
name: "translated: mobile",
description: "translated: All mobile members",
members: new Set([2, 31]),
};
user_groups.get_user_group_from_id = () => group_data;
let cancel_fade_out_called = false;
let instructions_fade_out_called = false;
$(cancel_selector).show();
$(cancel_selector).fadeOut = () => {
cancel_fade_out_called = true;
};
$(instructions_selector).fadeOut = () => {
instructions_fade_out_called = true;
};
// Cancel button removed if user group if user group has no changes.
const $fake_this = $.create("fake-#update_cancel_button");
handler_name.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
// Check if cancel button removed if user group error is showing.
$(user_group_selector + " .user-group-status").show();
cancel_fade_out_called = false;
instructions_fade_out_called = false;
handler_name.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
// Check for handler_desc to achieve 100% coverage.
cancel_fade_out_called = false;
instructions_fade_out_called = false;
handler_desc.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
})();
(function test_user_groups_save_group_changes_triggered() {
const handler_name = $(user_group_selector).get_on_handler("blur", ".name");
const handler_desc = $(user_group_selector).get_on_handler("blur", ".description");
const $sib_des = $(description_selector);
const $sib_name = $(name_selector);
$sib_name.text($t({defaultMessage: "mobile"}));
$sib_des.text($t({defaultMessage: "All mobile members"}));
const group_data = {members: new Set([2, 31])};
user_groups.get_user_group_from_id = () => group_data;
let api_endpoint_called = false;
let cancel_fade_out_called = false;
let saved_fade_to_called = false;
let instructions_fade_out_called = false;
$(instructions_selector).fadeOut = () => {
instructions_fade_out_called = true;
};
$(cancel_selector).fadeOut = () => {
cancel_fade_out_called = true;
};
$(saved_selector).css = (data) => {
assert.equal(typeof data, "object");
assert.equal(data.display, "inline-block");
assert.equal(data.opacity, "0");
return $(saved_selector);
};
$(saved_selector).fadeTo = () => {
saved_fade_to_called = true;
return $(saved_selector);
};
channel.patch = (opts) => {
assert.equal(opts.url, "/json/user_groups/1");
assert.equal(opts.data.name, "translated: mobile");
assert.equal(opts.data.description, "translated: All mobile members");
api_endpoint_called = true;
(function test_post_success() {
set_global("setTimeout", (func) => {
func();
});
opts.success();
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
assert.ok(saved_fade_to_called);
})();
(function test_post_error() {
const $user_group_error = $(user_group_selector + " .user-group-status");
$user_group_error.show();
ui_report.error = (error_msg, error_obj, ele) => {
assert.equal(error_msg, "translated HTML: Failed");
assert.deepEqual(error_obj, {responseJson: {msg: "fake-msg"}});
assert.equal(ele, $user_group_error);
};
opts.error({responseJson: {msg: "fake-msg"}});
assert.ok($user_group_error.visible());
})();
};
const $fake_this = $.create("fake-#user-groups_blur_name");
$fake_this.closest = () => [];
$fake_this.set_parents_result(user_group_selector, $(user_group_selector));
const event = {
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
api_endpoint_called = false;
handler_name.call($fake_this, event);
assert.ok(api_endpoint_called);
// Check API endpoint isn't called if name and desc haven't changed.
group_data.name = "translated: mobile";
group_data.description = "translated: All mobile members";
api_endpoint_called = false;
handler_name.call($fake_this, event);
assert.ok(!api_endpoint_called);
// Check for handler_desc to achieve 100% coverage.
api_endpoint_called = false;
handler_desc.call($fake_this, event);
assert.ok(!api_endpoint_called);
})();
(function test_user_groups_save_member_changes_triggered() {
const handler = $(user_group_selector).get_on_handler("blur", ".input");
const realm_user_group = {
id: 1,
name: "Mobile",
description: "All mobile people",
members: new Set([2, 4]),
};
user_groups.get_user_group_from_id = (id) => {
assert.equal(id, 1);
return realm_user_group;
};
let cancel_fade_out_called = false;
let saved_fade_to_called = false;
let instructions_fade_out_called = false;
$(instructions_selector).fadeOut = () => {
instructions_fade_out_called = true;
};
$(cancel_selector).fadeOut = () => {
cancel_fade_out_called = true;
};
$(saved_selector).css = () => $(saved_selector);
$(saved_selector).fadeTo = () => {
saved_fade_to_called = true;
return $(saved_selector);
};
let api_endpoint_called = false;
channel.post = (opts) => {
assert.equal(opts.url, "/json/user_groups/1/members");
assert.equal(opts.data.add, "[31]");
assert.equal(opts.data.delete, "[4]");
api_endpoint_called = true;
(function test_post_success() {
opts.success();
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
assert.ok(saved_fade_to_called);
})();
};
const $fake_this = $.create("fake-#user-groups_blur_input");
$fake_this.set_parents_result(user_group_selector, $(user_group_selector));
$fake_this.closest = () => [];
const event = {
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
api_endpoint_called = false;
handler.call($fake_this, event);
assert.ok(api_endpoint_called);
})();
});

View File

@ -4,6 +4,7 @@ const {strict: assert} = require("assert");
const {zrequire} = require("./lib/namespace");
const {run_test} = require("./lib/test");
const blueslip = require("./lib/zblueslip");
const {page_params} = require("./lib/zpage_params");
const people = zrequire("people");
@ -102,6 +103,9 @@ test("append", () => {
assert.ok(appended);
assert.ok(cleared);
blueslip.expect("warn", "Undefined user in function append_user");
user_pill.append_user(undefined, pill_widget);
});
test("get_items", () => {