diff --git a/frontend_tests/node_tests/settings_bots.js b/frontend_tests/node_tests/settings_bots.js index a901a00937..f472c00810 100644 --- a/frontend_tests/node_tests/settings_bots.js +++ b/frontend_tests/node_tests/settings_bots.js @@ -2,7 +2,7 @@ const {strict: assert} = require("assert"); -const {mock_cjs, mock_esm, zrequire} = require("../zjsunit/namespace"); +const {mock_cjs, zrequire} = require("../zjsunit/namespace"); const {run_test} = require("../zjsunit/test"); const $ = require("../zjsunit/zjquery"); const {page_params} = require("../zjsunit/zpage_params"); @@ -19,8 +19,6 @@ const bot_data_params = { ], }; -const avatar = mock_esm("../../static/js/avatar"); - function ClipboardJS(sel) { assert.equal(sel, "#copy_zuliprc"); } @@ -90,59 +88,14 @@ test("generate_botserverrc_content", () => { assert.equal(content, expected); }); -function test_create_bot_type_input_box_toggle(f) { - const $create_payload_url = $("#create_payload_url"); - const $payload_url_inputbox = $("#payload_url_inputbox"); - const $config_inputbox = $("#config_inputbox"); - const EMBEDDED_BOT_TYPE = "4"; - const OUTGOING_WEBHOOK_BOT_TYPE = "3"; - const GENERIC_BOT_TYPE = "1"; - - $("#create_bot_type :selected").val(EMBEDDED_BOT_TYPE); - f(); - assert.ok(!$create_payload_url.hasClass("required")); - assert.ok(!$payload_url_inputbox.visible()); - assert.ok($("#select_service_name").hasClass("required")); - assert.ok($("#service_name_list").visible()); - assert.ok($config_inputbox.visible()); - - $("#create_bot_type :selected").val(OUTGOING_WEBHOOK_BOT_TYPE); - f(); - assert.ok($create_payload_url.hasClass("required")); - assert.ok($payload_url_inputbox.visible()); - assert.ok(!$config_inputbox.visible()); - - $("#create_bot_type :selected").val(GENERIC_BOT_TYPE); - f(); - assert.ok(!$create_payload_url.hasClass("required")); - assert.ok(!$payload_url_inputbox.visible()); - assert.ok(!$config_inputbox.visible()); -} - -test("test tab clicks", ({override}) => { - override($.validator, "addMethod", () => {}); - - $("#create_bot_form").validate = () => {}; - - $("#config_inputbox").children = () => { - const $mock_children = { - hide: () => {}, - }; - return $mock_children; - }; - - override(avatar, "build_bot_create_widget", () => {}); - +test("test tab clicks", () => { settings_bots.set_up(); - test_create_bot_type_input_box_toggle(() => $("#create_bot_type").trigger("change")); - function click_on_tab($tab_elem) { $tab_elem.trigger("click"); } const tabs = { - $add: $("#bots_lists_navbar .add-a-new-bot-tab"), $active: $("#bots_lists_navbar .active-bots-tab"), $inactive: $("#bots_lists_navbar .inactive-bots-tab"), }; @@ -155,35 +108,21 @@ test("test tab clicks", ({override}) => { }; const forms = { - $add: $("#add-a-new-bot-form"), $active: $("#active_bots_list"), $inactive: $("#inactive_bots_list"), }; - click_on_tab(tabs.$add); - assert.ok(tabs.$add.hasClass("active")); - assert.ok(!tabs.$active.hasClass("active")); - assert.ok(!tabs.$inactive.hasClass("active")); - - assert.ok(forms.$add.visible()); - assert.ok(!forms.$active.visible()); - assert.ok(!forms.$inactive.visible()); - click_on_tab(tabs.$active); - assert.ok(!tabs.$add.hasClass("active")); assert.ok(tabs.$active.hasClass("active")); assert.ok(!tabs.$inactive.hasClass("active")); - assert.ok(!forms.$add.visible()); assert.ok(forms.$active.visible()); assert.ok(!forms.$inactive.visible()); click_on_tab(tabs.$inactive); - assert.ok(!tabs.$add.hasClass("active")); assert.ok(!tabs.$active.hasClass("active")); assert.ok(tabs.$inactive.hasClass("active")); - assert.ok(!forms.$add.visible()); assert.ok(!forms.$active.visible()); assert.ok(forms.$inactive.visible()); }); diff --git a/frontend_tests/puppeteer_tests/settings.ts b/frontend_tests/puppeteer_tests/settings.ts index 0b470c2f84..1b4ec7910c 100644 --- a/frontend_tests/puppeteer_tests/settings.ts +++ b/frontend_tests/puppeteer_tests/settings.ts @@ -94,15 +94,26 @@ async function test_get_api_key(page: Page): Promise { } async function test_webhook_bot_creation(page: Page): Promise { + await page.click("#bot-settings .add-a-new-bot"); + await common.wait_for_micromodal_to_open(page); + assert.strictEqual( + await common.get_text_from_selector(page, ".dialog_heading"), + "Add a new bot", + "Unexpected title for deactivate user modal", + ); + assert.strictEqual( + await common.get_text_from_selector(page, "#dialog_widget_modal .dialog_submit_button"), + "Add", + "Deactivate button has incorrect text.", + ); await common.fill_form(page, "#create_bot_form", { bot_name: "Bot 1", bot_short_name: "1", bot_type: OUTGOING_WEBHOOK_BOT_TYPE, payload_url: "http://hostname.example.com/bots/followup", }); - - await page.waitForSelector("#create_bot_button", {visible: true}); - await page.click("#create_bot_button"); + await page.click("#dialog_widget_modal .dialog_submit_button"); + await common.wait_for_micromodal_to_close(page); const bot_email = "1-bot@zulip.testserver"; const download_zuliprc_selector = `.download_bot_zuliprc[data-email="${CSS.escape( @@ -123,16 +134,25 @@ async function test_webhook_bot_creation(page: Page): Promise { } async function test_normal_bot_creation(page: Page): Promise { - await page.click(".add-a-new-bot-tab"); - await page.waitForSelector("#create_bot_button", {visible: true}); - + await page.click("#bot-settings .add-a-new-bot"); + await common.wait_for_micromodal_to_open(page); + assert.strictEqual( + await common.get_text_from_selector(page, ".dialog_heading"), + "Add a new bot", + "Unexpected title for deactivate user modal", + ); + assert.strictEqual( + await common.get_text_from_selector(page, "#dialog_widget_modal .dialog_submit_button"), + "Add", + "Deactivate button has incorrect text.", + ); await common.fill_form(page, "#create_bot_form", { bot_name: "Bot 2", bot_short_name: "2", bot_type: GENERIC_BOT_TYPE, }); - - await page.click("#create_bot_button"); + await page.click("#dialog_widget_modal .dialog_submit_button"); + await common.wait_for_micromodal_to_close(page); const bot_email = "2-bot@zulip.testserver"; const download_zuliprc_selector = `.download_bot_zuliprc[data-email="${CSS.escape( @@ -238,7 +258,6 @@ async function test_invalid_edit_bot_form(page: Page): Promise { async function test_your_bots_section(page: Page): Promise { await page.click('[data-section="your-bots"]'); - await page.click(".add-a-new-bot-tab"); await test_webhook_bot_creation(page); await test_normal_bot_creation(page); await test_botserverrc(page); diff --git a/static/js/settings_bots.js b/static/js/settings_bots.js index ec560d5ee6..4b190aee75 100644 --- a/static/js/settings_bots.js +++ b/static/js/settings_bots.js @@ -2,6 +2,7 @@ import ClipboardJS from "clipboard"; import $ from "jquery"; import render_settings_deactivation_bot_modal from "../templates/confirm_dialog/confirm_deactivate_bot.hbs"; +import render_add_new_bot_form from "../templates/settings/add_new_bot_form.hbs"; import render_bot_avatar_row from "../templates/settings/bot_avatar_row.hbs"; import render_edit_bot_form from "../templates/settings/edit_bot_form.hbs"; import render_settings_edit_embedded_bot_service from "../templates/settings/edit_embedded_bot_service.hbs"; @@ -15,7 +16,6 @@ import {csrf_token} from "./csrf"; import * as dialog_widget from "./dialog_widget"; import {DropdownListWidget} from "./dropdown_list_widget"; import {$t, $t_html} from "./i18n"; -import * as loading from "./loading"; import {page_params} from "./page_params"; import * as people from "./people"; import * as settings_config from "./settings_config"; @@ -23,7 +23,6 @@ import * as ui_report from "./ui_report"; import * as user_profile from "./user_profile"; const OUTGOING_WEBHOOK_BOT_TYPE = "3"; -const GENERIC_BOT_TYPE = "1"; const EMBEDDED_BOT_TYPE = "4"; export function hide_errors() { @@ -32,18 +31,9 @@ export function hide_errors() { } const focus_tab = { - add_a_new_bot_tab() { - $("#bots_lists_navbar .active").removeClass("active"); - $("#bots_lists_navbar .add-a-new-bot-tab").addClass("active"); - $("#add-a-new-bot-form").show(); - $("#active_bots_list").hide(); - $("#inactive_bots_list").hide(); - hide_errors(); - }, active_bots_tab() { $("#bots_lists_navbar .active").removeClass("active"); $("#bots_lists_navbar .active-bots-tab").addClass("active"); - $("#add-a-new-bot-form").hide(); $("#active_bots_list").show(); $("#inactive_bots_list").hide(); hide_errors(); @@ -51,7 +41,6 @@ const focus_tab = { inactive_bots_tab() { $("#bots_lists_navbar .active").removeClass("active"); $("#bots_lists_navbar .inactive-bots-tab").addClass("active"); - $("#add-a-new-bot-form").hide(); $("#active_bots_list").hide(); $("#inactive_bots_list").show(); hide_errors(); @@ -81,12 +70,9 @@ function add_bot_row(info) { } } -function is_local_part(value, element) { +function is_local_part(value) { // Adapted from Django's EmailValidator - return ( - this.optional(element) || - /^[\w!#$%&'*+/=?^`{|}~-]+(\.[\w!#$%&'*+/=?^`{|}~-]+)*$/i.test(value) - ); + return /^[\w!#$%&'*+/=?^`{|}~-]+(\.[\w!#$%&'*+/=?^`{|}~-]+)*$/i.test(value); } export function type_id_to_string(type_id) { @@ -113,11 +99,6 @@ export function render_bots() { }); user_owns_an_active_bot = user_owns_an_active_bot || elem.is_active; } - - if (can_create_new_bots() && !user_owns_an_active_bot) { - focus_tab.add_a_new_bot_tab(); - return; - } } export function generate_zuliprc_uri(bot_id) { @@ -215,14 +196,137 @@ export function update_bot_permissions_ui() { update_bot_settings_tip(); hide_errors(); $("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy); - if (!can_create_new_bots()) { - $("#create_bot_form").hide(); - $(".add-a-new-bot-tab").hide(); - focus_tab.active_bots_tab(); - } else { - $("#create_bot_form").show(); - $(".add-a-new-bot-tab").show(); +} + +export function add_a_new_bot() { + const html_body = render_add_new_bot_form({ + bot_types: page_params.bot_types, + realm_embedded_bots: page_params.realm_embedded_bots, + realm_bot_domain: page_params.realm_bot_domain, + }); + + let create_avatar_widget; + + function create_a_new_bot() { + const bot_type = $("#create_bot_type :selected").val(); + const full_name = $("#create_bot_name").val(); + const short_name = $("#create_bot_short_name").val() || $("#create_bot_short_name").text(); + const payload_url = $("#create_payload_url").val(); + const interface_type = $("#create_interface_type").val(); + const service_name = $("#select_service_name :selected").val(); + const formData = new FormData(); + + formData.append("csrfmiddlewaretoken", csrf_token); + formData.append("bot_type", bot_type); + formData.append("full_name", full_name); + formData.append("short_name", short_name); + + // If the selected bot_type is Outgoing webhook + if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { + formData.append("payload_url", JSON.stringify(payload_url)); + formData.append("interface_type", interface_type); + } else if (bot_type === EMBEDDED_BOT_TYPE) { + formData.append("service_name", service_name); + const config_data = {}; + $(`#config_inputbox [name*='${CSS.escape(service_name)}'] input`).each(function () { + config_data[$(this).attr("name")] = $(this).val(); + }); + formData.append("config_data", JSON.stringify(config_data)); + } + for (const [i, file] of Array.prototype.entries.call( + $("#bot_avatar_file_input")[0].files, + )) { + formData.append("file-" + i, file); + } + + channel.post({ + url: "/json/bots", + data: formData, + cache: false, + processData: false, + contentType: false, + success() { + hide_errors(); + create_avatar_widget.clear(); + dialog_widget.close_modal(); + }, + error(xhr) { + ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); + dialog_widget.hide_dialog_spinner(); + }, + }); } + + function set_up_form_fields() { + $("#payload_url_inputbox").hide(); + $("#create_payload_url").val(""); + $("#service_name_list").hide(); + $("#config_inputbox").hide(); + const selected_embedded_bot = "converter"; + $("#select_service_name").val(selected_embedded_bot); // TODO: Use 'select a bot'. + $("#config_inputbox").children().hide(); + $(`[name*='${CSS.escape(selected_embedded_bot)}']`).show(); + + create_avatar_widget = avatar.build_bot_create_widget(); + + $("#create_bot_type").on("change", () => { + const bot_type = $("#create_bot_type :selected").val(); + // For "generic bot" or "incoming webhook" both these fields need not be displayed. + $("#service_name_list").hide(); + $("#select_service_name").removeClass("required"); + $("#config_inputbox").hide(); + + $("#payload_url_inputbox").hide(); + $("#create_payload_url").removeClass("required"); + if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { + $("#payload_url_inputbox").show(); + $("#create_payload_url").addClass("required"); + } else if (bot_type === EMBEDDED_BOT_TYPE) { + $("#service_name_list").show(); + $("#select_service_name").addClass("required"); + $("#select_service_name").trigger("change"); + $("#config_inputbox").show(); + } + }); + + $("#select_service_name").on("change", () => { + $("#config_inputbox").children().hide(); + const selected_bot = $("#select_service_name :selected").val(); + $(`[name*='${CSS.escape(selected_bot)}']`).show(); + }); + } + + function validate_input(e) { + e.preventDefault(); + e.stopPropagation(); + + const bot_short_name = $("#create_bot_short_name").val(); + + if (is_local_part(bot_short_name)) { + return true; + } + ui_report.error( + $t_html({ + defaultMessage: "Please only use characters that are valid in an email address", + }), + undefined, + $("#dialog_error"), + ); + return false; + } + + dialog_widget.launch({ + form_id: "create_bot_form", + help_link: "/help/add-a-bot-or-integration", + html_body, + html_heading: $t_html({defaultMessage: "Add a new bot"}), + html_submit_button: $t_html({defaultMessage: "Add"}), + loading_spinner: true, + on_click: create_a_new_bot, + on_shown: () => $("#create_bot_type").trigger("focus"), + post_render: set_up_form_fields, + validate_input, + }); } export function confirm_bot_deactivation(bot_id, handle_confirm, loading_spinner) { @@ -382,15 +486,6 @@ export function show_edit_bot_info_modal(user_id, from_user_info_popover) { } export function set_up() { - $("#payload_url_inputbox").hide(); - $("#create_payload_url").val(""); - $("#service_name_list").hide(); - $("#config_inputbox").hide(); - const selected_embedded_bot = "converter"; - $("#select_service_name").val(selected_embedded_bot); // TODO: Use 'select a bot'. - $("#config_inputbox").children().hide(); - $(`[name*='${CSS.escape(selected_embedded_bot)}']`).show(); - $("#download_botserverrc").on("click", function () { const OUTGOING_WEBHOOK_BOT_TYPE_INT = 3; let content = ""; @@ -413,117 +508,6 @@ export function set_up() { focus_tab.active_bots_tab(); render_bots(); - $.validator.addMethod( - "bot_local_part", - function (value, element) { - return is_local_part.call(this, value + "-bot", element); - }, - "Please only use characters that are valid in an email address", - ); - - const create_avatar_widget = avatar.build_bot_create_widget(); - - const GENERIC_INTERFACE = "1"; - - $("#create_bot_form").validate({ - errorClass: "text-error", - success() { - hide_errors(); - }, - submitHandler() { - const bot_type = $("#create_bot_type :selected").val(); - const full_name = $("#create_bot_name").val(); - const short_name = - $("#create_bot_short_name").val() || $("#create_bot_short_name").text(); - const payload_url = $("#create_payload_url").val(); - const interface_type = $("#create_interface_type").val(); - const service_name = $("#select_service_name :selected").val(); - const formData = new FormData(); - const $spinner = $(".create_bot_spinner"); - - formData.append("csrfmiddlewaretoken", csrf_token); - formData.append("bot_type", bot_type); - formData.append("full_name", full_name); - formData.append("short_name", short_name); - - // If the selected bot_type is Outgoing webhook - if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { - formData.append("payload_url", JSON.stringify(payload_url)); - formData.append("interface_type", interface_type); - } else if (bot_type === EMBEDDED_BOT_TYPE) { - formData.append("service_name", service_name); - const config_data = {}; - $(`#config_inputbox [name*='${CSS.escape(service_name)}'] input`).each(function () { - config_data[$(this).attr("name")] = $(this).val(); - }); - formData.append("config_data", JSON.stringify(config_data)); - } - for (const [i, file] of Array.prototype.entries.call( - $("#bot_avatar_file_input")[0].files, - )) { - formData.append("file-" + i, file); - } - loading.make_indicator($spinner, {text: $t({defaultMessage: "Creating bot"})}); - channel.post({ - url: "/json/bots", - data: formData, - cache: false, - processData: false, - contentType: false, - success() { - hide_errors(); - $("#create_bot_name").val(""); - $("#create_bot_short_name").val(""); - $("#create_payload_url").val(""); - $("#payload_url_inputbox").hide(); - $("#config_inputbox").hide(); - $(`[name*='${CSS.escape(service_name)}'] input`).each(function () { - $(this).val(""); - }); - $("#create_bot_type").val(GENERIC_BOT_TYPE); - $("#select_service_name").val("converter"); // TODO: Later we can change this to hello bot or similar - $("#service_name_list").hide(); - $("#create_bot_button").show(); - $("#create_interface_type").val(GENERIC_INTERFACE); - create_avatar_widget.clear(); - focus_tab.active_bots_tab(); - }, - error(xhr) { - $("#bot_table_error").text(JSON.parse(xhr.responseText).msg).show(); - }, - complete() { - loading.destroy_indicator($spinner); - }, - }); - }, - }); - - $("#create_bot_type").on("change", () => { - const bot_type = $("#create_bot_type :selected").val(); - // For "generic bot" or "incoming webhook" both these fields need not be displayed. - $("#service_name_list").hide(); - $("#select_service_name").removeClass("required"); - $("#config_inputbox").hide(); - - $("#payload_url_inputbox").hide(); - $("#create_payload_url").removeClass("required"); - if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { - $("#payload_url_inputbox").show(); - $("#create_payload_url").addClass("required"); - } else if (bot_type === EMBEDDED_BOT_TYPE) { - $("#service_name_list").show(); - $("#select_service_name").addClass("required"); - $("#select_service_name").trigger("change"); - $("#config_inputbox").show(); - } - }); - - $("#select_service_name").on("change", () => { - $("#config_inputbox").children().hide(); - const selected_bot = $("#select_service_name :selected").val(); - $(`[name*='${CSS.escape(selected_bot)}']`).show(); - }); - $("#active_bots_list").on("click", "button.deactivate_bot", (e) => { const bot_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); @@ -601,12 +585,6 @@ export function set_up() { }, }); - $("#bots_lists_navbar .add-a-new-bot-tab").on("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - focus_tab.add_a_new_bot_tab(); - }); - $("#bots_lists_navbar .active-bots-tab").on("click", (e) => { e.preventDefault(); e.stopPropagation(); @@ -618,4 +596,10 @@ export function set_up() { e.stopPropagation(); focus_tab.inactive_bots_tab(); }); + + $("#bot-settings .add-a-new-bot").on("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + add_a_new_bot(); + }); } diff --git a/static/styles/settings.css b/static/styles/settings.css index 32538ed790..a2b38800c1 100644 --- a/static/styles/settings.css +++ b/static/styles/settings.css @@ -692,6 +692,10 @@ input[type="checkbox"] { max-height: calc(95vh - 280px); } +#bot-settings .add-a-new-bot { + margin-bottom: 2px; +} + .bots_list { display: none; list-style-type: none; diff --git a/static/templates/settings/add_new_bot_form.hbs b/static/templates/settings/add_new_bot_form.hbs new file mode 100644 index 0000000000..6be3189626 --- /dev/null +++ b/static/templates/settings/add_new_bot_form.hbs @@ -0,0 +1,73 @@ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + -bot@{{ realm_bot_domain }} +
+ +
+
+
+
+ + +
+
+
+ + +
+
+
+
+ {{#each realm_embedded_bots as |bot index|}} + {{#each bot.config as |config_value config_key|}} + {{> ../embedded_bot_config_item botname=bot.name key=config_key value=config_value}} + {{/each}} + {{/each}} +
+
+ +
+ + + ({{t "Optional" }}) +
+

+

+

+
+
diff --git a/static/templates/settings/bot_settings.hbs b/static/templates/settings/bot_settings.hbs index 01531e1460..3b14566482 100644 --- a/static/templates/settings/bot_settings.hbs +++ b/static/templates/settings/bot_settings.hbs @@ -9,7 +9,9 @@ {{/tr}}
- +
+ +
{{t 'Download config of all active outgoing webhook bots in Zulip Botserver format.' }} @@ -21,7 +23,6 @@
    @@ -31,86 +32,5 @@
- -
-
-
-
- - -
-
- - -
-
- - -
-
-
- - - -bot@{{ page_params.realm_bot_domain }} -
- -
-
-
-
- - -
-
-
- - -
-
-
-
- {{#each page_params.realm_embedded_bots as |bot index|}} - {{#each bot.config as |config_value config_key|}} - {{> ../embedded_bot_config_item botname=bot.name key=config_key value=config_value}} - {{/each}} - {{/each}} -
-
-
- - - ({{t "Optional" }}) -
-

-

-

- -
-
-
-
-