settings_account: Convert module to typescript.

This commit is contained in:
evykassirer 2024-10-20 13:17:06 -07:00 committed by Tim Abbott
parent 8eb0ca3a7c
commit 35424adcc3
6 changed files with 170 additions and 104 deletions

View File

@ -210,7 +210,7 @@ EXEMPT_FILES = make_set(
"web/src/sentry.ts",
"web/src/server_events.js",
"web/src/settings.js",
"web/src/settings_account.js",
"web/src/settings_account.ts",
"web/src/settings_bots.ts",
"web/src/settings_components.ts",
"web/src/settings_emoji.ts",

View File

@ -77,23 +77,22 @@ export function append_custom_profile_fields(element_id: string, user_id: number
}
}
export type PillUpdateField = {
type: number;
field_data: string;
hint: string;
id: number;
name: string;
order: number;
required: boolean;
display_in_profile_summary?: boolean | undefined;
};
export function initialize_custom_user_type_fields(
element_id: string,
user_id: number,
is_target_element_editable: boolean,
pill_update_handler?: (
field: {
type: number;
field_data: string;
hint: string;
id: number;
name: string;
order: number;
required: boolean;
display_in_profile_summary?: boolean | undefined;
},
pills: UserPillWidget,
) => void,
pill_update_handler?: (field: PillUpdateField, pills: UserPillWidget) => void,
): Map<number, UserPillWidget> {
const field_types = realm.custom_profile_field_types;
const user_pills = new Map<number, UserPillWidget>();

View File

@ -746,6 +746,9 @@ export function dispatch_normal_event(event) {
break;
}
// TODO/typescript: Move privacy_setting_name_schema and PrivacySettingName
// here from `settings_account` when this file is converted to typescript,
// and use them instead of `privacy_settings`.
const privacy_settings = [
"send_stream_typing_notifications",
"send_private_typing_notifications",

View File

@ -1,4 +1,6 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import {z} from "zod";
import render_change_email_modal from "../templates/change_email_modal.hbs";
import render_demo_organization_add_email_modal from "../templates/demo_organization_add_email_modal.hbs";
@ -7,11 +9,11 @@ import render_settings_api_key_modal from "../templates/settings/api_key_modal.h
import render_settings_dev_env_email_access from "../templates/settings/dev_env_email_access.hbs";
import * as avatar from "./avatar";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as common from "./common";
import {csrf_token} from "./csrf";
import * as custom_profile_fields_ui from "./custom_profile_fields_ui";
import type {PillUpdateField} from "./custom_profile_fields_ui";
import * as dialog_widget from "./dialog_widget";
import {$t_html} from "./i18n";
import * as keydown_util from "./keydown_util";
@ -29,13 +31,17 @@ import * as ui_report from "./ui_report";
import * as ui_util from "./ui_util";
import * as user_deactivation_ui from "./user_deactivation_ui";
import * as user_pill from "./user_pill";
import type {UserPillWidget} from "./user_pill";
import * as user_profile from "./user_profile";
import {user_settings} from "./user_settings";
import * as util from "./util";
let password_quality; // Loaded asynchronously
let password_quality:
| ((password: string, $bar: JQuery | undefined, $password_field: JQuery) => boolean)
| undefined; // Loaded asynchronously
let user_avatar_widget_created = false;
export function update_email(new_email) {
export function update_email(new_email: string): void {
const $email_input = $("#change_email_button");
if ($email_input) {
@ -43,7 +49,7 @@ export function update_email(new_email) {
}
}
export function update_full_name(new_full_name) {
export function update_full_name(new_full_name: string): void {
// Arguably, this should work more like how the `update_email`
// flow works, where we update the name in the modal on open,
// rather than updating it here, but this works.
@ -53,7 +59,7 @@ export function update_full_name(new_full_name) {
}
}
export function update_name_change_display() {
export function update_name_change_display(): void {
if ($("#user_details_section").length === 0) {
return;
}
@ -67,7 +73,7 @@ export function update_name_change_display() {
}
}
export function update_email_change_display() {
export function update_email_change_display(): void {
if ($("#user_details_section").length === 0) {
return;
}
@ -81,24 +87,27 @@ export function update_email_change_display() {
}
}
function display_avatar_upload_complete() {
function display_avatar_upload_complete(): void {
$("#user-avatar-upload-widget .upload-spinner-background").css({visibility: "hidden"});
$("#user-avatar-upload-widget .image-upload-text").show();
$("#user-avatar-upload-widget .image-delete-button").show();
}
function display_avatar_upload_started() {
function display_avatar_upload_started(): void {
$("#user-avatar-source").hide();
$("#user-avatar-upload-widget .upload-spinner-background").css({visibility: "visible"});
$("#user-avatar-upload-widget .image-upload-text").hide();
$("#user-avatar-upload-widget .image-delete-button").hide();
}
function upload_avatar($file_input) {
function upload_avatar($file_input: JQuery<HTMLInputElement>): void {
const form_data = new FormData();
assert(csrf_token !== undefined);
form_data.append("csrfmiddlewaretoken", csrf_token);
for (const [i, file] of Array.prototype.entries.call($file_input[0].files)) {
const files = util.the($file_input).files;
assert(files !== null);
for (const [i, file] of [...files].entries()) {
form_data.append("file-" + i, file);
}
display_avatar_upload_started();
@ -119,16 +128,17 @@ function upload_avatar($file_input) {
if (current_user.avatar_source === "G") {
$("#user-avatar-source").show();
}
if (xhr.responseJSON?.msg) {
const parsed = z.object({msg: z.string()}).safeParse(xhr.responseJSON);
if (parsed.success) {
const $error = $("#user-avatar-upload-widget .image_file_input_error");
$error.text(xhr.responseJSON.msg);
$error.text(parsed.data.msg);
$error.show();
}
},
});
}
export function update_avatar_change_display() {
export function update_avatar_change_display(): void {
if ($("#user-avatar-upload-widget").length === 0) {
return;
}
@ -137,7 +147,7 @@ export function update_avatar_change_display() {
$("#user-avatar-upload-widget .image_upload_button").addClass("hide");
$("#user-avatar-upload-widget .image-disabled").removeClass("hide");
} else {
if (user_avatar_widget_created === false) {
if (!user_avatar_widget_created) {
avatar.build_user_avatar_widget(upload_avatar);
user_avatar_widget_created = true;
}
@ -146,7 +156,7 @@ export function update_avatar_change_display() {
}
}
export function update_account_settings_display() {
export function update_account_settings_display(): void {
if ($("#user_details_section").length === 0) {
return;
}
@ -156,7 +166,7 @@ export function update_account_settings_display() {
update_avatar_change_display();
}
export function maybe_update_deactivate_account_button() {
export function maybe_update_deactivate_account_button(): void {
if (!current_user.is_owner) {
return;
}
@ -173,7 +183,7 @@ export function maybe_update_deactivate_account_button() {
}
}
export function update_send_read_receipts_tooltip() {
export function update_send_read_receipts_tooltip(): void {
if (realm.realm_enable_read_receipts) {
$("#send_read_receipts_label .settings-info-icon").hide();
} else {
@ -181,11 +191,14 @@ export function update_send_read_receipts_tooltip() {
}
}
function settings_change_error(message_html, xhr) {
function settings_change_error(message_html: string, xhr?: JQuery.jqXHR): void {
ui_report.error(message_html, xhr, $("#account-settings-status").expectOne());
}
function update_custom_profile_field(field, method) {
function update_custom_profile_field(
field: CustomProfileFieldData,
method: channel.AjaxRequestHandler,
): void {
let data;
if (method === channel.del) {
data = JSON.stringify([field.id]);
@ -199,17 +212,21 @@ function update_custom_profile_field(field, method) {
settings_ui.do_settings_change(method, "/json/users/me/profile_data", {data}, $spinner_element);
}
function update_user_custom_profile_fields(fields, method) {
if (method === undefined) {
blueslip.error("Undefined method in update_user_custom_profile_fields");
}
type CustomProfileFieldData = {
id: number;
value?: number[] | string;
};
function update_user_custom_profile_fields(
fields: CustomProfileFieldData[],
method: channel.AjaxRequestHandler,
): void {
for (const field of fields) {
update_custom_profile_field(field, method);
}
}
function update_user_type_field(field, pills) {
function update_user_type_field(field: PillUpdateField, pills: UserPillWidget): void {
const user_ids = user_pill.get_user_ids(pills);
if (user_ids.length < 1) {
update_user_custom_profile_fields([{id: field.id}], channel.del);
@ -218,7 +235,7 @@ function update_user_type_field(field, pills) {
}
}
export function add_custom_profile_fields_to_settings() {
export function add_custom_profile_fields_to_settings(): void {
if (!overlays.settings_open()) {
return;
}
@ -226,7 +243,9 @@ export function add_custom_profile_fields_to_settings() {
const element_id = "#profile-settings .custom-profile-fields-form";
$(element_id).empty();
const pill_update_handler = (field, pills) => update_user_type_field(field, pills);
const pill_update_handler = (field: PillUpdateField, pills: UserPillWidget): void => {
update_user_type_field(field, pills);
};
custom_profile_fields_ui.append_custom_profile_fields(element_id, people.my_current_user_id());
custom_profile_fields_ui.initialize_custom_user_type_fields(
@ -239,14 +258,25 @@ export function add_custom_profile_fields_to_settings() {
custom_profile_fields_ui.initialize_custom_pronouns_type_fields(element_id);
}
export function hide_confirm_email_banner() {
export function hide_confirm_email_banner(): void {
if (!overlays.settings_open()) {
return;
}
$("#account-settings-status").hide();
}
export function update_privacy_settings_box(property) {
// TODO/typescript: Move these to server_events_dispatch when it's converted to typescript.
export const privacy_setting_name_schema = z.enum([
"send_stream_typing_notifications",
"send_private_typing_notifications",
"send_read_receipts",
"presence_enabled",
"email_address_visibility",
"allow_private_data_export",
]);
export type PrivacySettingName = z.infer<typeof privacy_setting_name_schema>;
export function update_privacy_settings_box(property: PrivacySettingName): void {
if (!overlays.settings_open()) {
return;
}
@ -256,19 +286,24 @@ export function update_privacy_settings_box(property) {
settings_components.set_input_element_value($input_elem, user_settings[property]);
}
export function set_up(load_password_quality) {
export function set_up(
load_password_quality: () => Promise<
(password: string, $bar: JQuery | undefined, $password_field: JQuery) => boolean
>,
): void {
// Add custom profile fields elements to user account settings.
add_custom_profile_fields_to_settings();
$("#account-settings-status").hide();
const setup_api_key_modal = () => {
function request_api_key(data) {
const setup_api_key_modal = (): void => {
function request_api_key(data: {password?: string}): void {
channel.post({
url: "/json/fetch_api_key",
data,
success(data) {
$("#get_api_key_password").val("");
$("#api_key_value").text(data.api_key);
const api_key = z.object({api_key: z.string()}).parse(data).api_key;
$("#api_key_value").text(api_key);
// The display property on the error bar is set to important
// so instead of making display: none !important we just
// remove it.
@ -298,14 +333,15 @@ export function set_up(load_password_quality) {
{tippy_tooltips: true},
);
function do_get_api_key() {
function do_get_api_key(): void {
$("#api_key_status").hide();
const data = {};
data.password = $("#get_api_key_password").val();
const data = {
password: $<HTMLInputElement>("input#get_api_key_password").val()!,
};
request_api_key(data);
}
if (realm.realm_password_auth_enabled === false) {
if (!realm.realm_password_auth_enabled) {
// Skip the password prompt step, since the user doesn't have one.
request_api_key({});
} else {
@ -334,11 +370,13 @@ export function set_up(load_password_quality) {
url: "/api/v1/users/me/api_key/regenerate",
headers: {Authorization: authorization_header},
success(data) {
$("#api_key_value").text(data.api_key);
const api_key = z.object({api_key: z.string()}).parse(data).api_key;
$("#api_key_value").text(api_key);
},
error(xhr) {
if (xhr.responseJSON?.msg) {
$("#user_api_key_error").text(xhr.responseJSON.msg).show();
const parsed = z.object({msg: z.string()}).safeParse(xhr.responseJSON);
if (parsed.success) {
$("#user_api_key_error").text(parsed.data.msg).show();
}
},
});
@ -378,7 +416,7 @@ export function set_up(load_password_quality) {
e.stopPropagation();
});
function clear_password_change() {
function clear_password_change(): void {
// Clear the password boxes so that passwords don't linger in the DOM
// for an XSS attacker to find.
common.reset_password_toggle_icons(
@ -393,7 +431,7 @@ export function set_up(load_password_quality) {
password_quality?.("", $("#pw_strength .bar"), $("#new_password"));
}
function change_password_post_render() {
function change_password_post_render(): void {
$("#change_password_modal")
.find("[data-micromodal-close]")
.on("click", () => {
@ -412,11 +450,11 @@ export function set_up(load_password_quality) {
clear_password_change();
}
$("#change_password").on("click", async (e) => {
$("#change_password").on("click", (e) => {
e.preventDefault();
e.stopPropagation();
function validate_input() {
function validate_input(): boolean {
const old_password = $("#old_password").val();
const new_password = $("#new_password").val();
@ -458,26 +496,29 @@ export function set_up(load_password_quality) {
},
});
if (realm.realm_password_auth_enabled !== false) {
if (realm.realm_password_auth_enabled) {
// zxcvbn.js is pretty big, and is only needed on password
// change, so load it asynchronously.
password_quality = await load_password_quality();
$("#pw_strength .bar").removeClass("hide");
void (async () => {
password_quality = await load_password_quality();
$("#pw_strength .bar").removeClass("hide");
$("#new_password").on("input", () => {
const $field = $("#new_password");
password_quality($field.val(), $("#pw_strength .bar"), $field);
});
$("#new_password").on("input", () => {
const $field = $<HTMLInputElement>("input#new_password");
assert(password_quality !== undefined);
password_quality($field.val()!, $("#pw_strength .bar"), $field);
});
})();
}
});
function do_change_password() {
function do_change_password(): void {
const $change_password_error = $("#change_password_modal").find("#dialog_error");
$change_password_error.hide();
const data = {
old_password: $("#old_password").val(),
new_password: $("#new_password").val(),
new_password: $<HTMLInputElement>("input#new_password").val()!,
};
const $new_pw_field = $("#new_password");
@ -521,9 +562,9 @@ export function set_up(load_password_quality) {
$("#full_name").on("change", (e) => {
e.preventDefault();
e.stopPropagation();
const data = {};
data.full_name = $("#full_name").val();
const data = {
full_name: $("#full_name").val(),
};
settings_ui.do_settings_change(
channel.patch,
@ -533,10 +574,11 @@ export function set_up(load_password_quality) {
);
});
function do_change_email() {
function do_change_email(): void {
const $change_email_error = $("#change_email_modal").find("#dialog_error");
const data = {};
data.email = $("#change_email_form").find("input[name='email']").val();
const data = {
email: $("#change_email_form").find<HTMLInputElement>("input[name='email']").val(),
};
const opts = {
success_continuation() {
@ -582,20 +624,21 @@ export function set_up(load_password_quality) {
form_id: "change_email_form",
on_click: do_change_email,
on_shown() {
ui_util.place_caret_at_end($("#change_email_form input")[0]);
ui_util.place_caret_at_end(util.the($("#change_email_form input")));
},
update_submit_disabled_state_on_change: true,
});
}
});
function do_demo_organization_add_email(e) {
function do_demo_organization_add_email(e: JQuery.ClickEvent): void {
e.preventDefault();
e.stopPropagation();
const $change_email_error = $("#demo_organization_add_email_modal").find("#dialog_error");
const data = {};
data.email = $("#demo_organization_add_email").val();
data.full_name = $("#demo_organization_update_full_name").val();
const data = {
email: $<HTMLInputElement>("input#demo_organization_add_email").val(),
full_name: $("#demo_organization_update_full_name").val(),
};
const opts = {
success_continuation() {
@ -632,10 +675,12 @@ export function set_up(load_password_quality) {
e.preventDefault();
e.stopPropagation();
function demo_organization_add_email_post_render() {
function demo_organization_add_email_post_render(): void {
// Disable submit button if either input is an empty string.
const $add_email_element = $("#demo_organization_add_email");
const $add_name_element = $("#demo_organization_update_full_name");
const $add_email_element = $<HTMLInputElement>("input#demo_organization_add_email");
const $add_name_element = $<HTMLInputElement>(
"input#demo_organization_update_full_name",
);
const $demo_organization_submit_button = $(
"#demo_organization_add_email_modal .dialog_submit_button",
@ -645,7 +690,8 @@ export function set_up(load_password_quality) {
$("#demo_organization_add_email_form input").on("input", () => {
$demo_organization_submit_button.prop(
"disabled",
$add_email_element.val().trim() === "" || $add_name_element.val().trim() === "",
$add_email_element.val()!.trim() === "" ||
$add_name_element.val()!.trim() === "",
);
});
}
@ -667,26 +713,33 @@ export function set_up(load_password_quality) {
form_id: "demo_organization_add_email_form",
on_click: do_demo_organization_add_email,
on_shown() {
ui_util.place_caret_at_end($("#demo_organization_add_email_form input")[0]);
ui_util.place_caret_at_end(
util.the($("#demo_organization_add_email_form input")),
);
},
post_render: demo_organization_add_email_post_render,
});
}
});
$("#profile-settings").on("click", ".custom_user_field .remove_date", (e) => {
e.preventDefault();
e.stopPropagation();
const $field = $(this).closest(".custom_user_field").expectOne();
const field_id = Number.parseInt($field.attr("data-field-id"), 10);
update_user_custom_profile_fields([{id: field_id}], channel.del);
});
$("#profile-settings").on(
"click",
".custom_user_field .remove_date",
function (this: HTMLElement, e) {
e.preventDefault();
e.stopPropagation();
const $field = $(this).closest(".custom_user_field").expectOne();
const field_id = Number.parseInt($field.attr("data-field-id")!, 10);
update_user_custom_profile_fields([{id: field_id}], channel.del);
},
);
$("#profile-settings").on("change", ".custom_user_field_value", function (e) {
const fields = [];
const value = $(this).val();
$("#profile-settings").on("change", ".custom_user_field_value", function (this: HTMLElement) {
const fields: CustomProfileFieldData[] = [];
const value = $(this).val()!;
assert(typeof value === "string");
const field_id = Number.parseInt(
$(this).closest(".custom_user_field").attr("data-field-id"),
$(this).closest(".custom_user_field").attr("data-field-id")!,
10,
);
if (value) {
@ -708,7 +761,7 @@ export function set_up(load_password_quality) {
e.preventDefault();
e.stopPropagation();
function handle_confirm() {
function handle_confirm(): void {
channel.del({
url: "/json/users/me",
success() {
@ -732,9 +785,15 @@ export function set_up(load_password_quality) {
)}</a>`,
},
);
let rendered_error_msg;
if (xhr.responseJSON?.code === "CANNOT_DEACTIVATE_LAST_USER") {
if (xhr.responseJSON.is_last_owner) {
let rendered_error_msg = "";
const parsed = z
.object({
code: z.literal("CANNOT_DEACTIVATE_LAST_USER"),
is_last_owner: z.boolean(),
})
.safeParse(xhr.responseJSON);
if (parsed.success) {
if (parsed.data.is_last_owner) {
rendered_error_msg = error_last_owner;
} else {
rendered_error_msg = error_last_user;
@ -776,7 +835,7 @@ export function set_up(load_password_quality) {
$("#user_timezone").val(user_settings.timezone);
$("#user_timezone").on("change", function (e) {
$<HTMLSelectElement>("select#user_timezone").on("change", function (e) {
e.preventDefault();
e.stopPropagation();
@ -790,13 +849,13 @@ export function set_up(load_password_quality) {
);
});
$("#privacy_settings_box").on("change", "input", (e) => {
$("#privacy_settings_box").on("change", "input", function (this: HTMLInputElement, e) {
e.preventDefault();
e.stopPropagation();
const $input_elem = $(this);
const setting_name = $input_elem.attr("name");
const checked = $input_elem.prop("checked");
const setting_name = $input_elem.attr("name")!;
const checked = util.the($input_elem).checked;
const data = {[setting_name]: checked};
settings_ui.do_settings_change(
@ -809,7 +868,7 @@ export function set_up(load_password_quality) {
$("#user_email_address_visibility").val(user_settings.email_address_visibility);
$("#user_email_address_visibility").on("change", function (e) {
$<HTMLSelectElement>("select#user_email_address_visibility").on("change", function (e) {
e.preventDefault();
e.stopPropagation();

View File

@ -105,7 +105,12 @@ export function encode_zuliprc_as_url(zuliprc: string): string {
return "data:application/octet-stream;charset=utf-8," + encodeURIComponent(zuliprc);
}
export function generate_zuliprc_content(bot: bot_data.Bot): string {
export function generate_zuliprc_content(bot: {
bot_type?: number;
user_id: number;
email: string;
api_key: string;
}): string {
let token;
// For outgoing webhooks, include the token in the zuliprc.
// It's needed for authenticating to the Botserver.

View File

@ -920,7 +920,7 @@ function get_human_profile_data(fields_user_pills: Map<number, user_pill.UserPil
to see how the form is built.
TODO: Ideally, this logic would be cleaned up or deduplicated with
the settings_account.js logic.
the settings_account.ts logic.
*/
const new_profile_data = [];
$("#edit-user-form .custom_user_field_value").each(function () {