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))