ts: Covert `dialog_widget.js` to typescript.

Created `WidgetConfig` type which has all the parameter that can be
passed launch(). Except html_heading, html_body, and on_click all
other fields are optional. Therefore, removed checking of undefined
mandatory fields as it will be done by typescript.

Co-authored-by: Hardik Dharmani <Ddharmani99@gmail.com>
This commit is contained in:
Hardik Dharmani 2023-03-30 21:51:28 +05:30 committed by Tim Abbott
parent fcbc6dcae4
commit ebc07c0dd7
4 changed files with 64 additions and 27 deletions

View File

@ -139,7 +139,7 @@ js_rules = RuleList(
"web/src/portico", "web/src/portico",
"web/src/lightbox.js", "web/src/lightbox.js",
"web/src/ui_report.ts", "web/src/ui_report.ts",
"web/src/dialog_widget.js", "web/src/dialog_widget.ts",
"web/tests/", "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.", "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.",

View File

@ -78,7 +78,7 @@ EXEMPT_FILES = make_set(
"web/src/debug.ts", "web/src/debug.ts",
"web/src/deprecated_feature_notice.js", "web/src/deprecated_feature_notice.js",
"web/src/desktop_integration.js", "web/src/desktop_integration.js",
"web/src/dialog_widget.js", "web/src/dialog_widget.ts",
"web/src/drafts.js", "web/src/drafts.js",
"web/src/dropdown_list_widget.js", "web/src/dropdown_list_widget.js",
"web/src/echo.js", "web/src/echo.js",

View File

@ -112,6 +112,9 @@ function call(args) {
return $.ajax(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) { export function get(options) {
const args = {type: "GET", dataType: "json", ...options}; const args = {type: "GET", dataType: "json", ...options};
return call(args); return call(args);

View File

@ -2,7 +2,6 @@ import $ from "jquery";
import render_dialog_widget from "../templates/dialog_widget.hbs"; import render_dialog_widget from "../templates/dialog_widget.hbs";
import * as blueslip from "./blueslip";
import {$t_html} from "./i18n"; import {$t_html} from "./i18n";
import * as loading from "./loading"; import * as loading from "./loading";
import * as overlays from "./overlays"; 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. * 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<string, never>;
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_submit_button span").show();
$("#dialog_widget_modal .modal__btn").prop("disabled", false); $("#dialog_widget_modal .modal__btn").prop("disabled", false);
@ -50,7 +89,7 @@ export function hide_dialog_spinner() {
loading.destroy_indicator($spinner); loading.destroy_indicator($spinner);
} }
export function show_dialog_spinner() { export function show_dialog_spinner(): void {
$(".dialog_submit_button span").hide(); $(".dialog_submit_button span").hide();
// Disable both the buttons. // Disable both the buttons.
$("#dialog_widget_modal .modal__btn").prop("disabled", true); $("#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. // 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}); overlays.close_modal("dialog_widget_modal", {on_hidden: on_hidden_callback});
} }
export function launch(conf) { export function launch(conf: WidgetConfig): void {
const mandatory_fields = [ // Mandatory fields:
// * html_heading
// * html_body
// * on_click
// The html_ fields should be safe HTML. If callers // The html_ fields should be safe HTML. If callers
// interpolate user data into strings, they should use // interpolate user data into strings, they should use
// templates. // templates.
"html_heading",
"html_body",
"on_click",
];
// Optional parameters: // Optional parameters:
// * html_submit_button: Submit button text. // * 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_hide: Callback to run when the modal is triggered to hide.
// * on_hidden: Callback to run when the modal is hidden. // * on_hidden: Callback to run when the modal is hidden.
// * post_render: Callback to run after the modal body is added to DOM. // * post_render: Callback to run after the modal body is added to DOM.
// * loading_spinner: Whether to show a loading spinner inside the
for (const f of mandatory_fields) { // submit button when clicked.
if (conf[f] === undefined) {
blueslip.error("programmer omitted " + f);
}
}
const html_submit_button = conf.html_submit_button || $t_html({defaultMessage: "Save changes"}); const html_submit_button = conf.html_submit_button || $t_html({defaultMessage: "Save changes"});
const html = render_dialog_widget({ const html = render_dialog_widget({
@ -156,26 +190,26 @@ export function launch(conf) {
} }
export function submit_api_request( export function submit_api_request(
request_method, request_method: AjaxRequest,
url, url: string,
data = {}, data = {},
{ {
failure_msg_html = $t_html({defaultMessage: "Failed"}), failure_msg_html = $t_html({defaultMessage: "Failed"}),
success_continuation, success_continuation,
error_continuation, error_continuation,
} = {}, }: RequestOpts = {},
) { ): void {
show_dialog_spinner(); show_dialog_spinner();
request_method({ request_method({
url, url,
data, data,
success(response_data) { success(response_data?: string) {
close_modal(); close_modal();
if (success_continuation !== undefined) { if (success_continuation !== undefined) {
success_continuation(response_data); success_continuation(response_data);
} }
}, },
error(xhr) { error(xhr?: JQuery.jqXHR) {
ui_report.error(failure_msg_html, xhr, $("#dialog_error")); ui_report.error(failure_msg_html, xhr, $("#dialog_error"));
hide_dialog_spinner(); hide_dialog_spinner();
if (error_continuation !== undefined) { if (error_continuation !== undefined) {