From 87dee7a9b2832f01f596db95dfcc2db42ad4010a Mon Sep 17 00:00:00 2001 From: adnan-td Date: Sun, 24 Mar 2024 00:06:45 +0530 Subject: [PATCH] invite_user_modal: Replaced email text_area with input_pill. This makes the widget considerably more attractive, and probably a bit more usable. Fixes #29391. --- tools/test-js-with-node | 1 + web/src/email_pill.ts | 57 +++++++++++++++++++++++++++++ web/src/input_pill.ts | 17 ++++++++- web/src/invite.ts | 46 +++++++++++++++++------ web/styles/input_pill.css | 4 ++ web/templates/invite_user_modal.hbs | 4 +- web/tests/input_pill.test.js | 37 +++++++++++++++++++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 web/src/email_pill.ts diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 29af45387a..0ba16913b9 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -102,6 +102,7 @@ EXEMPT_FILES = make_set( "web/src/dropdown_widget.ts", "web/src/echo.js", "web/src/electron_bridge.d.ts", + "web/src/email_pill.ts", "web/src/emoji_picker.js", "web/src/emojisets.ts", "web/src/favicon.ts", diff --git a/web/src/email_pill.ts b/web/src/email_pill.ts new file mode 100644 index 0000000000..df8de6be95 --- /dev/null +++ b/web/src/email_pill.ts @@ -0,0 +1,57 @@ +import type {InputPillConfig, InputPillContainer, InputPillItem} from "./input_pill"; +import * as input_pill from "./input_pill"; + +type EmailPill = { + email: string; +}; + +export type EmailPillWidget = InputPillContainer; + +const email_regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function create_item_from_email( + email: string, + current_items: InputPillItem[], +): InputPillItem | undefined { + if (!email_regex.test(email)) { + return undefined; + } + + const existing_emails = current_items.map((item) => item.email); + if (existing_emails.includes(email)) { + return undefined; + } + + return { + type: "email", + display_value: email, + email, + }; +} + +export function get_email_from_item(item: InputPillItem): string { + return item.email; +} + +export function get_current_email( + pill_container: input_pill.InputPillContainer, +): string | null { + const current_text = pill_container.getCurrentText(); + if (current_text !== null && email_regex.test(current_text)) { + return current_text; + } + return null; +} + +export function create_pills( + $pill_container: JQuery, + pill_config?: InputPillConfig | undefined, +): input_pill.InputPillContainer { + const pill_container = input_pill.create({ + $container: $pill_container, + pill_config, + create_item_from_text: create_item_from_email, + get_text_from_item: get_email_from_item, + }); + return pill_container; +} diff --git a/web/src/input_pill.ts b/web/src/input_pill.ts index 1a795913ad..7adc29a44e 100644 --- a/web/src/input_pill.ts +++ b/web/src/input_pill.ts @@ -43,6 +43,7 @@ type InputPill = { }; type InputPillStore = { + onTextInputHook?: () => void; pills: InputPill[]; pill_config: InputPillCreateOptions["pill_config"]; $parent: JQuery; @@ -72,9 +73,11 @@ export type InputPillContainer = { items: () => InputPillItem[]; onPillCreate: (callback: () => void) => void; onPillRemove: (callback: (pill: InputPill) => void) => void; + onTextInputHook: (callback: () => void) => void; createPillonPaste: (callback: () => void) => void; clear: () => void; clear_text: () => void; + getCurrentText: () => string | null; is_pending: () => boolean; _get_pills_for_testing: () => InputPill[]; }; @@ -109,6 +112,10 @@ export function create(opts: InputPillCreateOptions): InputPillContainer(opts: InputPillCreateOptions): InputPillContainer(opts: InputPillCreateOptions): InputPillContainer(opts: InputPillCreateOptions): InputPillContainer(opts: InputPillCreateOptions): InputPillContainer(opts: InputPillCreateOptions): InputPillContainer("select:not([multiple])#invite_as").val()!, @@ -79,7 +81,19 @@ function get_common_invitation_data(): { invite_as, stream_ids: JSON.stringify(stream_ids), invite_expires_in_minutes: JSON.stringify(expires_in), + invitee_emails: pills + .items() + .map((pill) => email_pill.get_email_from_item(pill)) + .join(","), }; + const current_email = email_pill.get_current_email(pills); + if (current_email) { + if (pills.items().length === 0) { + data.invitee_emails = current_email; + } else { + data.invitee_emails += "," + current_email; + } + } return data; } @@ -100,16 +114,14 @@ function submit_invitation_form(): void { "select:not([multiple])#expires_in", ); const $invite_status = $("#dialog_error"); - const $invitee_emails = $("textarea#invitee_emails"); const data = get_common_invitation_data(); - data.invitee_emails = $invitee_emails.val()!; void channel.post({ url: "/json/invites", data, beforeSend, success() { - const number_of_invites_sent = $invitee_emails.val()!.split(/[\n,]/).length; + const number_of_invites_sent = pills.items().length; ui_report.success( $t_html( { @@ -120,7 +132,7 @@ function submit_invitation_form(): void { ), $invite_status, ); - $invitee_emails.val(""); + pills.clear(); if (page_params.development_environment) { const rendered_email_msg = render_settings_dev_env_email_access(); @@ -171,7 +183,9 @@ function submit_invitation_form(): void { ui_report.message(error_response, $invite_status, "alert-error"); if (response_body.sent_invitations) { - $invitee_emails.val(invitee_emails_errored.join("\n")); + for (const email of invitee_emails_errored) { + pills.appendValue(email); + } } } }, @@ -327,7 +341,14 @@ function open_invite_user_modal(e: JQuery.ClickEvent): void const $expires_in = $( "select:not([multiple])#expires_in", ); - const $invitee_emails = $("textarea#invitee_emails"); + const $pill_container = $("#invitee_emails_container .pill-container"); + pills = input_pill.create({ + $container: $pill_container, + create_item_from_text: email_pill.create_item_from_email, + get_text_from_item: email_pill.get_email_from_item, + }); + const $pill_input = $("#invitee_emails_container .pill-container .input"); + $pill_input.trigger("focus"); $("#invite-user-modal .dialog_submit_button").prop("disabled", true); $("#email_invite_radio").prop("checked", true); @@ -340,8 +361,6 @@ function open_invite_user_modal(e: JQuery.ClickEvent): void const user_has_email_set = !settings_data.user_email_not_configured(); - autosize($invitee_emails.trigger("focus")); - set_custom_time_inputs_visibility(); set_expires_on_text(); set_streams_to_join_list_visibility(); @@ -358,11 +377,16 @@ function open_invite_user_modal(e: JQuery.ClickEvent): void function toggle_invite_submit_button(): void { $("#invite-user-modal .dialog_submit_button").prop( "disabled", - $invitee_emails.val()!.trim() === "" && + pills.items().length === 0 && + email_pill.get_current_email(pills) === null && !$("#generate_multiuse_invite_radio").is(":checked"), ); } + pills.onPillCreate(toggle_invite_submit_button); + pills.onPillRemove(toggle_invite_submit_button); + pills.onTextInputHook(toggle_invite_submit_button); + $("#invite-user-modal").on("input", "input, textarea, select", () => { toggle_invite_submit_button(); }); diff --git a/web/styles/input_pill.css b/web/styles/input_pill.css index ec00e8b513..c457410f15 100644 --- a/web/styles/input_pill.css +++ b/web/styles/input_pill.css @@ -141,6 +141,10 @@ } } +#invitee_emails_container .pill-container { + width: 100%; +} + .deactivated-pill { background-color: hsl(0deg 86% 86%) !important; } diff --git a/web/templates/invite_user_modal.hbs b/web/templates/invite_user_modal.hbs index afac75baa5..7c548aa1a9 100644 --- a/web/templates/invite_user_modal.hbs +++ b/web/templates/invite_user_modal.hbs @@ -29,7 +29,9 @@
- +
+
+
diff --git a/web/tests/input_pill.test.js b/web/tests/input_pill.test.js index 822357023e..01d610ff60 100644 --- a/web/tests/input_pill.test.js +++ b/web/tests/input_pill.test.js @@ -639,3 +639,40 @@ run_test("appendValue/clear", ({mock_template}) => { assert.deepEqual(removed_colors, ["blue", "yellow", "red"]); assert.equal($pill_input[0].textContent, ""); }); + +run_test("getCurrentText/onTextInputHook", ({mock_template}) => { + mock_template("input_pill.hbs", true, (data, html) => { + assert.equal(typeof data.display_value, "string"); + return html; + }); + + const info = set_up(); + const config = info.config; + const items = info.items; + const $pill_input = info.$pill_input; + const $container = info.$container; + + const widget = input_pill.create(config); + widget.appendValue("blue,red"); + assert.deepEqual(widget.items(), [items.blue, items.red]); + + const onTextInputHook = () => { + assert.deepEqual(widget.items(), [items.blue, items.red]); + }; + widget.onTextInputHook(onTextInputHook); + + $pill_input.text("yellow"); + assert.equal(widget.getCurrentText(), "yellow"); + + const key_handler = $container.get_on_handler("keydown", ".input"); + key_handler({ + key: " ", + preventDefault: noop, + }); + key_handler({ + key: ",", + preventDefault: noop, + }); + + assert.deepEqual(widget.items(), [items.blue, items.red, items.yellow]); +});