From 635d99ba72a2bac95b564e73aa5e9aa744fb6d5f Mon Sep 17 00:00:00 2001 From: Evy Kassirer Date: Thu, 11 Sep 2025 15:01:34 -0700 Subject: [PATCH] zform: Convert module to typescript. --- docs/subsystems/widgets.md | 4 ++-- tools/test-js-with-node | 2 +- web/src/submessage.ts | 31 ++++++++++++++------------- web/src/widgetize.ts | 8 +------ web/src/widgets.js | 2 +- web/src/{zform.js => zform.ts} | 39 +++++++++++++++++++--------------- 6 files changed, 43 insertions(+), 43 deletions(-) rename web/src/{zform.js => zform.ts} (52%) diff --git a/docs/subsystems/widgets.md b/docs/subsystems/widgets.md index 24409580b2..804a88dabe 100644 --- a/docs/subsystems/widgets.md +++ b/docs/subsystems/widgets.md @@ -54,7 +54,7 @@ Some important code entities for the widget implementation are: - `web/src/submessage.js` - `web/src/poll_widget.js` - `web/src/widgetize.ts` -- `web/src/zform.js` +- `web/src/zform.ts` - `web/templates/widgets/` - `zerver/lib/widget.py` - `zerver/views/submessage.py` @@ -308,7 +308,7 @@ widgets.todo = todo_widget; widgets.zform = zform; ``` -The code in `web/src/zform.js` renders the form (not +The code in `web/src/zform.ts` renders the form (not shown here) and then sets up a click handler like below: ```js diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 405a5fefd1..2717695a31 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -307,7 +307,7 @@ EXEMPT_FILES = make_set( "web/src/views_util.ts", "web/src/widget_modal.ts", "web/src/zcommand.ts", - "web/src/zform.js", + "web/src/zform.ts", "web/src/zulip_test.ts", # Test library code isn't always fully used. "web/tests/lib/example_user.cjs", diff --git a/web/src/submessage.ts b/web/src/submessage.ts index 894884b1b1..bbd352f137 100644 --- a/web/src/submessage.ts +++ b/web/src/submessage.ts @@ -12,20 +12,18 @@ import * as widgetize from "./widgetize.ts"; export type Submessage = z.infer; -export const zform_widget_extra_data_schema = z.nullable( - z.object({ - choices: z.array( - z.object({ - type: z.string(), - long_name: z.string(), - reply: z.string(), - short_name: z.string(), - }), - ), - heading: z.string(), - type: z.literal("choices"), - }), -); +export const zform_widget_extra_data_schema = z.object({ + choices: z.array( + z.object({ + type: z.string(), + long_name: z.string(), + reply: z.string(), + short_name: z.string(), + }), + ), + heading: z.string(), + type: z.literal("choices"), +}); const poll_widget_extra_data_schema = z.nullable( z.object({ @@ -38,7 +36,10 @@ const widget_data_event_schema = z.object({ sender_id: z.number(), data: z.discriminatedUnion("widget_type", [ z.object({widget_type: z.literal("poll"), extra_data: poll_widget_extra_data_schema}), - z.object({widget_type: z.literal("zform"), extra_data: zform_widget_extra_data_schema}), + z.object({ + widget_type: z.literal("zform"), + extra_data: z.nullable(zform_widget_extra_data_schema), + }), z.object({ widget_type: z.literal("todo"), extra_data: todo_widget_extra_data_schema, diff --git a/web/src/widgetize.ts b/web/src/widgetize.ts index d969c95a92..7a0ff52319 100644 --- a/web/src/widgetize.ts +++ b/web/src/widgetize.ts @@ -5,13 +5,7 @@ import * as message_lists from "./message_lists.ts"; import type {Message} from "./message_store.ts"; import type {Event, PollWidgetExtraData, PollWidgetOutboundData} from "./poll_widget.ts"; import type {TodoWidgetExtraData, TodoWidgetOutboundData} from "./todo_widget.ts"; - -// TODO: This ZFormExtraData type should be moved to web/src/zform.js when it will be migrated -type ZFormExtraData = { - type: string; - heading: string; - choices: {type: string; reply: string; long_name: string; short_name: string}[]; -}; +import type {ZFormExtraData} from "./zform.ts"; type WidgetExtraData = PollWidgetExtraData | TodoWidgetExtraData | ZFormExtraData | null; diff --git a/web/src/widgets.js b/web/src/widgets.js index 334c35923c..bc6917e887 100644 --- a/web/src/widgets.js +++ b/web/src/widgets.js @@ -1,7 +1,7 @@ import * as poll_widget from "./poll_widget.ts"; import * as todo_widget from "./todo_widget.ts"; import * as widgetize from "./widgetize.ts"; -import * as zform from "./zform.js"; +import * as zform from "./zform.ts"; export function initialize() { widgetize.widgets.set("poll", poll_widget); diff --git a/web/src/zform.js b/web/src/zform.ts similarity index 52% rename from web/src/zform.js rename to web/src/zform.ts index 6f5f938c94..65b02dc969 100644 --- a/web/src/zform.js +++ b/web/src/zform.ts @@ -1,43 +1,51 @@ import $ from "jquery"; +import type * as z from "zod/mini"; import render_widgets_zform_choices from "../templates/widgets/zform_choices.hbs"; import * as blueslip from "./blueslip.ts"; +import type {Message} from "./message_store.ts"; +import type {Event} from "./poll_widget.ts"; import {zform_widget_extra_data_schema} from "./submessage.ts"; import * as transmit from "./transmit.ts"; -export function activate(opts) { - const self = {}; +export type ZFormExtraData = z.infer; +export function activate(opts: { + $elem: JQuery; + extra_data: unknown; + message: Message; +}): {handle_events: (events: Event[]) => void} | undefined { const $outer_elem = opts.$elem; const parse_result = zform_widget_extra_data_schema.safeParse(opts.extra_data); if (!parse_result.success) { - blueslip.warn("invalid zform extra data", parse_result.error.issues); + blueslip.warn("invalid zform extra data", {issues: parse_result.error.issues}); return undefined; } const {data} = parse_result; - function make_choices(data) { + function make_choices(data: ZFormExtraData): JQuery { // Assign idx values to each of our choices so that // our template can create data-idx values for our // JS code to use later. - for (const [idx, choice] of data.choices.entries()) { - choice.idx = idx; - } + const data_with_choices_with_idx = { + ...data, + choices: data.choices.map((choice, idx) => ({...choice, idx})), + }; - const html = render_widgets_zform_choices(data); + const html = render_widgets_zform_choices(data_with_choices_with_idx); const $elem = $(html); $elem.find("button").on("click", (e) => { e.stopPropagation(); // Grab our index from the markup. - const idx = $(e.target).attr("data-idx"); + const idx = Number.parseInt($(e.target).attr("data-idx")!, 10); // Use the index from the markup to dereference our // data structure. - const reply_content = data.choices[idx].reply; + const reply_content = data.choices[idx]!.reply; transmit.reply_message(opts.message, reply_content); }); @@ -45,16 +53,13 @@ export function activate(opts) { return $elem; } - function render() { - let rendered_widget; - + function render(): void { if (data.type === "choices") { - rendered_widget = make_choices(data); - $outer_elem.html(rendered_widget); + $outer_elem.html(make_choices(data).html()); } } - self.handle_events = function (events) { + const handle_events = function (events: Event[]): void { if (events) { blueslip.info("unexpected"); } @@ -63,5 +68,5 @@ export function activate(opts) { render(); - return self; + return {handle_events}; }