settings_bots: Move "Add a new bot" tab inside a modal.

"Add a new bot" tab from personal `settings > bots` moving this
into a modal form, so we can trigger this form from other places
too without duplicating the code.

Fixes part of #20309.
This commit is contained in:
Yogesh Sirsat
2022-09-15 17:37:39 +05:30
committed by Tim Abbott
parent bca41fd29f
commit 6db88f0d39
6 changed files with 249 additions and 310 deletions

View File

@@ -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());
});

View File

@@ -94,15 +94,26 @@ async function test_get_api_key(page: Page): Promise<void> {
}
async function test_webhook_bot_creation(page: Page): Promise<void> {
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<void> {
}
async function test_normal_bot_creation(page: Page): Promise<void> {
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<void> {
async function test_your_bots_section(page: Page): Promise<void> {
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);

View File

@@ -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();
});
}

View File

@@ -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;

View File

@@ -0,0 +1,73 @@
<form id="create_bot_form" class="form-horizontal new-style">
<div class="new-bot-form">
<div class="input-group">
<label for="bot_type">
{{t "Bot type" }}
<i class="fa fa-question-circle settings-info-icon bot_type_tooltip tippy-zulip-tooltip" aria-hidden="true" data-tippy-content='{{t "Incoming webhooks can only send messages." }}'></i>
</label>
<select name="bot_type" id="create_bot_type">
{{#each bot_types}}
{{#if this.allowed}}
<option value="{{this.type_id}}">{{this.name}}</option>
{{/if}}
{{/each}}
</select>
</div>
<div class="input-group" id="service_name_list">
<label for="select_service_name">{{t "Bot"}}</label>
<select name="service_name" id="select_service_name">
{{#each realm_embedded_bots}}
<option value="{{this.name}}">{{this.name}}</option>
{{/each}}
</select>
</div>
<div class="input-group">
<label for="create_bot_name">{{t "Full name" }}</label>
<input type="text" name="bot_name" id="create_bot_name" class="required"
maxlength=100 placeholder="{{t 'Cookie Bot' }}" value="" />
<div><label for="create_bot_name" generated="true" class="text-error"></label></div>
</div>
<div class="input-group">
<label for="bot_short_name">{{t "Bot email (a-z, 0-9, and dashes only)" }}</label>
<input type="text" name="bot_short_name" id="create_bot_short_name" class="required bot_local_part"
placeholder="{{t 'cookie' }}" value="" />
-bot@{{ realm_bot_domain }}
<div>
<label for="create_bot_short_name" generated="true" class="text-error"></label>
</div>
</div>
<div id="payload_url_inputbox">
<div class="input-group">
<label for="create_payload_url">{{t "Endpoint URL" }}</label>
<input type="text" name="payload_url" id="create_payload_url"
maxlength=2083 placeholder="https://hostname.example.com" value="" />
<div><label for="create_payload_url" generated="true" class="text-error"></label></div>
</div>
<div class="input-group">
<label for="interface_type">{{t "Outgoing webhook message format" }}</label>
<select name="interface_type" id="create_interface_type">
<option value="1">Zulip</option>
<option value="2">{{t "Slack compatible" }}</option>
</select>
<div><label for="create_interface_type" generated="true" class="text-error"></label></div>
</div>
</div>
<div id="config_inputbox">
{{#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}}
</div>
<div class="input-group">
<label for="bot_avatar_file_input">Avatar</label>
<div id="bot_avatar_file"></div>
<input type="file" name="bot_avatar_file_input" class="notvisible" id="bot_avatar_file_input" value="{{t 'Upload profile picture' }}" />
<button class="button white rounded small btn-danger" style="display: none;" id="bot_avatar_clear_button">{{t "Clear profile picture" }}</button>
<button class="button white rounded" id="bot_avatar_upload_button">{{t "Customize profile picture" }}</button> ({{t "Optional" }})
</div>
<p>
<div id="bot_avatar_file_input_error" class="text-error"></div>
</p>
</div>
</form>

View File

@@ -9,7 +9,9 @@
{{/tr}}
</div>
<div class="tip bot-settings-tip"></div>
<div>
<button class="button rounded sea-green add-a-new-bot {{#unless can_create_new_bots}}hide{{/unless}}">{{t "Add a new bot" }}</button>
</div>
<div>
<span>{{t 'Download config of all active outgoing webhook bots in Zulip Botserver format.' }}</span>
<a type="submit" download="{{botserverrc}}" id= "download_botserverrc" class="btn" title="{{t 'Download botserverrc' }}">
@@ -21,7 +23,6 @@
<ul class="nav nav-tabs nav-justified" id="bots_lists_navbar">
<li class="active active-bots-tab"><a>{{t "Active bots" }}</a></li>
<li class="inactive-bots-tab"><a>{{t "Inactive bots" }}</a></li>
<li class="add-a-new-bot-tab {{#unless can_create_new_bots}}hide{{/unless}}"><a>{{t "Add a new bot" }}</a></li>
</ul>
<ol class="bots_list required-text" id="active_bots_list" data-empty="{{t 'You have no active bots.' }}">
@@ -31,86 +32,5 @@
</ol>
<div id="bot_table_error" class="alert alert-error hide"></div>
<div id="add-a-new-bot-form">
<form id="create_bot_form"
class="form-horizontal no-padding {{#unless can_create_new_bots}}hide{{/unless}}">
<div class="new-bot-form">
<div class="input-group">
<label for="bot_type">
{{t "Bot type" }}
<i class="fa fa-question-circle settings-info-icon bot_type_tooltip tippy-zulip-tooltip" aria-hidden="true" data-tippy-content='{{t "Incoming webhooks can only send messages." }}'></i>
</label>
<select name="bot_type" id="create_bot_type">
{{#each page_params.bot_types}}
{{#if this.allowed}}
<option value="{{this.type_id}}">{{this.name}}</option>
{{/if}}
{{/each}}
</select>
</div>
<div class="input-group" id="service_name_list">
<label for="select_service_name">{{t "Bot"}}</label>
<select name="service_name" id="select_service_name">
{{#each page_params.realm_embedded_bots}}
<option value="{{this.name}}">{{this.name}}</option>
{{/each}}
</select>
</div>
<div class="input-group">
<label for="create_bot_name">{{t "Full name" }}</label>
<input type="text" name="bot_name" id="create_bot_name" class="required"
maxlength=100 placeholder="{{t 'Cookie Bot' }}" value="" />
<div><label for="create_bot_name" generated="true" class="text-error"></label></div>
</div>
<div class="input-group">
<label for="bot_short_name">{{t "Bot email (a-z, 0-9, and dashes only)" }}</label>
<input type="text" name="bot_short_name" id="create_bot_short_name" class="required bot_local_part"
placeholder="{{t 'cookie' }}" value="" />
-bot@{{ page_params.realm_bot_domain }}
<div>
<label for="create_bot_short_name" generated="true" class="text-error"></label>
</div>
</div>
<div id="payload_url_inputbox">
<div class="input-group">
<label for="create_payload_url">{{t "Endpoint URL" }}</label>
<input type="text" name="payload_url" id="create_payload_url"
maxlength=2083 placeholder="https://hostname.example.com" value="" />
<div><label for="create_payload_url" generated="true" class="text-error"></label></div>
</div>
<div class="input-group">
<label for="interface_type">{{t "Outgoing webhook message format" }}</label>
<select name="interface_type" id="create_interface_type">
<option value="1">Zulip</option>
<option value="2">{{t "Slack compatible" }}</option>
</select>
<div><label for="create_interface_type" generated="true" class="text-error"></label></div>
</div>
</div>
<div id="config_inputbox">
{{#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}}
</div>
<div class="input-group">
<div id="bot_avatar_file"></div>
<input type="file" name="bot_avatar_file_input" class="notvisible" id="bot_avatar_file_input" value="{{t 'Upload profile picture' }}" />
<button class="button white rounded small btn-danger" style="display: none;" id="bot_avatar_clear_button">{{t "Clear profile picture" }}</button>
<button class="button white rounded" id="bot_avatar_upload_button">{{t "Customize profile picture" }}</button> ({{t "Optional" }})
</div>
<p>
<div id="bot_avatar_file_input_error" class="text-error"></div>
</p>
<button type="submit" class="button white rounded sea-green" id="create_bot_button">
{{t 'Create bot' }}
</button>
<div class="create_bot_spinner"></div>
</div>
</form>
</div>
</div>
</div>