diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index aa02f46d3a..f6eb74de8a 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -139,7 +139,7 @@ js_rules = RuleList( "web/src/portico", "web/src/lightbox.js", "web/src/ui_report.ts", - "web/src/dialog_widget.js", + "web/src/dialog_widget.ts", "web/tests/", }, "description": "Setting HTML content with jQuery .html() can lead to XSS security bugs. Consider .text() or using rendered_foo as a variable name if content comes from Handlebars and thus is already sanitized.", diff --git a/tools/test-js-with-node b/tools/test-js-with-node index f36101b21b..a137f586a1 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -78,7 +78,7 @@ EXEMPT_FILES = make_set( "web/src/debug.ts", "web/src/deprecated_feature_notice.js", "web/src/desktop_integration.js", - "web/src/dialog_widget.js", + "web/src/dialog_widget.ts", "web/src/drafts.js", "web/src/dropdown_list_widget.js", "web/src/echo.js", diff --git a/web/src/channel.js b/web/src/channel.js index 8e153d8b84..4d9a897ab3 100644 --- a/web/src/channel.js +++ b/web/src/channel.js @@ -112,6 +112,9 @@ function call(args) { return $.ajax(args); } +// TODO: When this file is converted to TypeScript, deduplicate the +// AjaxRequest type defined in dialog_widget.js. + export function get(options) { const args = {type: "GET", dataType: "json", ...options}; return call(args); diff --git a/web/src/dialog_widget.js b/web/src/dialog_widget.ts similarity index 77% rename from web/src/dialog_widget.js rename to web/src/dialog_widget.ts index f60d111c1c..ed64168561 100644 --- a/web/src/dialog_widget.js +++ b/web/src/dialog_widget.ts @@ -2,7 +2,6 @@ import $ from "jquery"; import render_dialog_widget from "../templates/dialog_widget.hbs"; -import * as blueslip from "./blueslip"; import {$t_html} from "./i18n"; import * as loading from "./loading"; import * as overlays from "./overlays"; @@ -42,7 +41,47 @@ import * as ui_report from "./ui_report"; * to DOM, it can do so by passing a post_render hook. */ -export function hide_dialog_spinner() { +type WidgetConfig = { + html_heading: string; + html_body: string; + on_click: (e: unknown) => void; + html_submit_button?: string; + close_on_submit?: boolean; + focus_submit_on_open?: boolean; + help_link?: string; + id?: string; + single_footer_button?: boolean; + form_id?: string; + validate_input?: (e: unknown) => boolean; + on_show?: () => void; + on_shown?: () => void; + on_hide?: () => void; + on_hidden?: () => void; + post_render?: () => void; + loading_spinner?: boolean; +}; + +// TODO: This type should probably be exported from channel.ts once +// that's converted to TypeScript. +type AjaxRequest = ({ + url, + data = {}, + success, + error, +}: { + url: string; + data?: Record; + success(response_data?: string): void; + error(xhr?: JQuery.jqXHR): void; +}) => void; + +type RequestOpts = { + failure_msg_html?: string; + success_continuation?: (response_data?: string) => void; + error_continuation?: (xhr?: JQuery.jqXHR) => void; +}; + +export function hide_dialog_spinner(): void { $(".dialog_submit_button span").show(); $("#dialog_widget_modal .modal__btn").prop("disabled", false); @@ -50,7 +89,7 @@ export function hide_dialog_spinner() { loading.destroy_indicator($spinner); } -export function show_dialog_spinner() { +export function show_dialog_spinner(): void { $(".dialog_submit_button span").hide(); // Disable both the buttons. $("#dialog_widget_modal .modal__btn").prop("disabled", true); @@ -65,19 +104,18 @@ export function show_dialog_spinner() { } // Supports a callback to be called once the modal finishes closing. -export function close_modal(on_hidden_callback) { +export function close_modal(on_hidden_callback?: () => void): void { overlays.close_modal("dialog_widget_modal", {on_hidden: on_hidden_callback}); } -export function launch(conf) { - const mandatory_fields = [ - // The html_ fields should be safe HTML. If callers - // interpolate user data into strings, they should use - // templates. - "html_heading", - "html_body", - "on_click", - ]; +export function launch(conf: WidgetConfig): void { + // Mandatory fields: + // * html_heading + // * html_body + // * on_click + // The html_ fields should be safe HTML. If callers + // interpolate user data into strings, they should use + // templates. // Optional parameters: // * html_submit_button: Submit button text. @@ -93,12 +131,8 @@ export function launch(conf) { // * on_hide: Callback to run when the modal is triggered to hide. // * on_hidden: Callback to run when the modal is hidden. // * post_render: Callback to run after the modal body is added to DOM. - - for (const f of mandatory_fields) { - if (conf[f] === undefined) { - blueslip.error("programmer omitted " + f); - } - } + // * loading_spinner: Whether to show a loading spinner inside the + // submit button when clicked. const html_submit_button = conf.html_submit_button || $t_html({defaultMessage: "Save changes"}); const html = render_dialog_widget({ @@ -156,26 +190,26 @@ export function launch(conf) { } export function submit_api_request( - request_method, - url, + request_method: AjaxRequest, + url: string, data = {}, { failure_msg_html = $t_html({defaultMessage: "Failed"}), success_continuation, error_continuation, - } = {}, -) { + }: RequestOpts = {}, +): void { show_dialog_spinner(); request_method({ url, data, - success(response_data) { + success(response_data?: string) { close_modal(); if (success_continuation !== undefined) { success_continuation(response_data); } }, - error(xhr) { + error(xhr?: JQuery.jqXHR) { ui_report.error(failure_msg_html, xhr, $("#dialog_error")); hide_dialog_spinner(); if (error_continuation !== undefined) {