diff --git a/api_docs/incoming-webhooks-walkthrough.md b/api_docs/incoming-webhooks-walkthrough.md index af14103b28..1427696c51 100644 --- a/api_docs/incoming-webhooks-walkthrough.md +++ b/api_docs/incoming-webhooks-walkthrough.md @@ -304,6 +304,15 @@ Currently configured preset URL options: `ignore_private_repositories` boolean parameter will be added to the [generated integration URL](/help/generate-integration-url). +- **`MAPPING`**: This preset is intended to be used for [chat-app + integrations](/integrations/communication) (like Slack), and adds a + special option, **Matching Zulip channel**, to the UI for where to send + Zulip notification messages. This special option maps the notification + messages to Zulip channels that match the messages' original channel + name in the third-party app. When selected, this requires setting a + single topic for notification messages, and adds `&mapping=channels` + to the [generated integration URL](/help/generate-integration-url). + ## Step 4: Manually testing the webhook For either one of the command line tools, first, you'll need to get an diff --git a/web/shared/icons/equal.svg b/web/shared/icons/equal.svg new file mode 100644 index 0000000000..b4d7d292ec --- /dev/null +++ b/web/shared/icons/equal.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/integration_url_modal.ts b/web/src/integration_url_modal.ts index f1e1101ee7..c07e965cdd 100644 --- a/web/src/integration_url_modal.ts +++ b/web/src/integration_url_modal.ts @@ -35,6 +35,7 @@ const url_options_schema = z.array(url_option_schema); const PresetUrlOption = { BRANCHES: "branches", + MAPPING: "mapping", }; export function show_generate_integration_url_modal(api_key: string): void { @@ -49,6 +50,10 @@ export function show_generate_integration_url_modal(api_key: string): void { unique_id: -1, is_direct_message: true, }; + const map_channels_option: Option = { + name: $t_html({defaultMessage: "Matching Zulip channel"}), + unique_id: -2, + }; const html_body = render_generate_integration_url_modal({ default_url_message, max_topic_length: realm.max_topic_length, @@ -125,6 +130,8 @@ export function show_generate_integration_url_modal(api_key: string): void { $config_element.find("#integration-url-all-branches").on("change", () => { show_branch_filtering_ui(); }); + } else if (option.key === PresetUrlOption.MAPPING) { + continue; } else if (option.validator === "check_bool") { const config_html = render_generate_integration_url_config_checkbox_modal({ key: option.key, @@ -241,10 +248,16 @@ export function show_generate_integration_url_modal(api_key: string): void { if (url_options) { for (const option of url_options) { let $input_element; - if ( - option.key === PresetUrlOption.BRANCHES && - !$("#integration-url-all-branches").prop("checked") - ) { + if (option.key === PresetUrlOption.MAPPING) { + const stream_input = stream_input_dropdown_widget.value(); + if (stream_input === map_channels_option?.unique_id) { + params.delete("stream"); + params.set(PresetUrlOption.MAPPING, "channels"); + } + } else if (option.key === PresetUrlOption.BRANCHES) { + if ($("#integration-url-all-branches").prop("checked")) { + continue; + } const $pill_container = $( "#integration-url-filter-branches .pill-container", ); @@ -319,9 +332,10 @@ export function show_generate_integration_url_modal(api_key: string): void { const selected_integration_data = realm.realm_incoming_webhook_bots.find( (bot) => bot.name === selected_integration, ); + const url_options = selected_integration_data?.url_options; - if (selected_integration_data?.url_options) { - render_url_options(selected_integration_data.url_options); + if (url_options) { + render_url_options(url_options); } dropdown.hide(); @@ -339,9 +353,37 @@ export function show_generate_integration_url_modal(api_key: string): void { }); stream_input_dropdown_widget.setup(); + function get_additional_stream_dropdown_options(): Option[] { + const additional_options: Option[] = []; + + const selected_integration = integration_input_dropdown_widget.value(); + const selected_integration_data = realm.realm_incoming_webhook_bots.find( + (bot) => bot.name === selected_integration, + ); + if (!selected_integration) { + return additional_options; + } + + const url_options = selected_integration_data?.url_options; + if (!url_options) { + return additional_options; + } + + const mapping_option = url_options?.find( + (option) => option.key === PresetUrlOption.MAPPING, + ); + + if (mapping_option) { + additional_options.push(map_channels_option); + } + return additional_options; + } + function get_options_for_stream_dropdown_widget(): Option[] { + const additional_options = get_additional_stream_dropdown_options(); const options = [ direct_messages_option, + ...additional_options, ...streams .filter((stream) => stream_data.can_post_messages_in_stream(stream)) .map((stream) => ({ @@ -365,6 +407,11 @@ export function show_generate_integration_url_modal(api_key: string): void { $override_topic.prop("checked", false).prop("disabled", true); $override_topic.closest(".input-group").addClass("control-label-disabled"); $topic_input.val(""); + } else if (user_selected_option === map_channels_option.unique_id) { + $override_topic.prop("checked", true).prop("disabled", true); + $override_topic.closest(".input-group").addClass("control-label-disabled"); + $topic_input.val(""); + $topic_input.parent().removeClass("hide"); } else { $override_topic.prop("disabled", false); $override_topic.closest(".input-group").removeClass("control-label-disabled"); diff --git a/web/templates/dropdown_list.hbs b/web/templates/dropdown_list.hbs index 6fdb568a33..1fea6300cd 100644 --- a/web/templates/dropdown_list.hbs +++ b/web/templates/dropdown_list.hbs @@ -36,6 +36,9 @@ {{t "Disable" }} {{/if}} + {{else if (eq unique_id -2)}} + {{!-- This is the option for PresetUrlOption.MAPPING --}} + {{name}} {{else}} {{#if bold_current_selection}} {{name}} diff --git a/zerver/lib/webhooks/common.py b/zerver/lib/webhooks/common.py index e646fba3fa..b987e6862e 100644 --- a/zerver/lib/webhooks/common.py +++ b/zerver/lib/webhooks/common.py @@ -59,6 +59,7 @@ OptionalUserSpecifiedTopicStr: TypeAlias = Annotated[str | None, ApiParamConfig( class PresetUrlOption(str, Enum): BRANCHES = "branches" IGNORE_PRIVATE_REPOSITORIES = "ignore_private_repositories" + MAPPING = "mapping" @dataclass @@ -96,6 +97,12 @@ class WebhookUrlOption: label="Exclude notifications from private repositories", validator=check_bool, ) + case PresetUrlOption.MAPPING: + return cls( + name=config.value, + label="", + validator=check_string, + ) raise AssertionError(_("Unknown 'PresetUrlOption': {config}").format(config=config))