integrations: Add URL option and UI for mapping messages to Zulip channels.

This commit adds a "mapping" URL option preset that adds "Matching Zulip
channel" option to the stream dropdown widget. When that option is
chosen from the dropdown, it adds another parameter to the integration
URL -- "&mapping=channels".

This "mapping" parameter is meant to be used by integrations like Slack
to identify whether the user wants to map Slack channels to different
Zulip channels or different topics within a single channel.

This adds an icon for the `mapping`s' drop down option in the "Where to
send notification" drop down field.

Co-authored-by: Pieter CK <pieterceka123@gmail.com>
Co-authored-by: Lauryn Menard <lauryn@zulip.com>
This commit is contained in:
Niloth P
2025-03-08 00:35:36 +05:30
committed by Tim Abbott
parent 3863f4d6a5
commit 7022811349
5 changed files with 75 additions and 6 deletions

View File

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

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M2.668 6c0-.367.298-.665.665-.665h9.334a.665.665 0 0 1 0 1.33H3.333A.665.665 0 0 1 2.668 6Zm0 4c0-.367.298-.665.665-.665h9.334a.665.665 0 0 1 0 1.33H3.333A.665.665 0 0 1 2.668 10Z"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -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");

View File

@@ -36,6 +36,9 @@
{{t "Disable" }}
{{/if}}
</span>
{{else if (eq unique_id -2)}}
{{!-- This is the option for PresetUrlOption.MAPPING --}}
<i class="zulip-icon zulip-icon-equal channel-privacy-type-icon" aria-hidden="true"></i> {{name}}
{{else}}
{{#if bold_current_selection}}
<span class="dropdown-list-bold-selected">{{name}}</span>

View File

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