FEATURE: Allow editing channel slug (#19948)

This commit introduces the ability to edit the channel
slug from the About tab for the chat channel when the user
is admin. Similar to the create channel modal functionality
introduced in 641e94f, if
the slug is left empty then we autogenerate a slug based
on the channel name, and if the user just changes the slug
manually we use that instead.

We do not do any link remapping or anything else of the
sort, when the category slug is changed that does not happen
either.
This commit is contained in:
Martin Brennan 2023-01-30 13:18:34 +10:00 committed by GitHub
parent 7ec6e6b3d0
commit db5ad34508
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 233 additions and 71 deletions

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
CHANNEL_EDITABLE_PARAMS = %i[name description]
CHANNEL_EDITABLE_PARAMS = %i[name description slug]
CATEGORY_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users allow_channel_wide_mentions]
class Chat::Api::ChatChannelsController < Chat::Api

View File

@ -205,6 +205,7 @@ module ChatPublisher
chat_channel_id: chat_channel.id,
name: chat_channel.title(acting_user),
description: chat_channel.description,
slug: chat_channel.slug,
},
permissions(chat_channel),
)

View File

@ -22,7 +22,7 @@
{{#if (chat-guardian "can-edit-chat-channel")}}
<div class="chat-form__label-actions">
<DButton
@class="edit-name-btn btn-flat"
@class="edit-name-slug-btn btn-flat"
@label="chat.channel_settings.edit"
@action={{if this.onEditChatChannelName this.onEditChatChannelName}}
/>
@ -33,6 +33,9 @@
<div class="channel-info-about-view__name">
{{replace-emoji this.channel.escapedTitle}}
</div>
<div class="channel-info-about-view__slug">
{{this.channel.slug}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,94 @@
import Controller from "@ember/controller";
import discourseDebounce from "discourse-common/lib/debounce";
import { ajax } from "discourse/lib/ajax";
import { cancel } from "@ember/runloop";
import { action, computed } from "@ember/object";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { inject as service } from "@ember/service";
export default class ChatChannelEditTitleController extends Controller.extend(
ModalFunctionality
) {
@service chatApi;
editedName = "";
editedSlug = "";
autoGeneratedSlug = "";
@computed("model.title", "editedName", "editedSlug")
get isSaveDisabled() {
return (
(this.model.title === this.editedName &&
this.model.slug === this.editedSlug) ||
this.editedName?.length > this.siteSettings.max_topic_title_length
);
}
onShow() {
this.setProperties({
editedName: this.model.title,
editedSlug: this.model.slug,
});
}
onClose() {
this.setProperties({
editedName: "",
editedSlug: "",
});
this.clearFlash();
}
@action
onSaveChatChannelName() {
return this.chatApi
.updateChannel(this.model.id, {
name: this.editedName,
slug: this.editedSlug || this.autoGeneratedSlug || this.model.slug,
})
.then((result) => {
this.model.set("title", result.channel.title);
this.send("closeModal");
})
.catch((event) => {
this.flash(extractError(event), "error");
});
}
@action
onChangeChatChannelName(title) {
this.clearFlash();
this._debouncedGenerateSlug(title);
}
@action
onChangeChatChannelSlug() {
this.clearFlash();
this._debouncedGenerateSlug(this.editedName);
}
_clearAutoGeneratedSlug() {
this.set("autoGeneratedSlug", "");
}
_debouncedGenerateSlug(name) {
cancel(this.generateSlugHandler);
this._clearAutoGeneratedSlug();
if (!name) {
return;
}
this.generateSlugHandler = discourseDebounce(
this,
this._generateSlug,
name,
300
);
}
// intentionally not showing AJAX error for this, we will autogenerate
// the slug server-side if they leave it blank
_generateSlug(name) {
ajax("/slugs.json", { type: "POST", data: { name } }).then((response) => {
this.set("autoGeneratedSlug", response.slug);
});
}
}

View File

@ -1,50 +0,0 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { inject as service } from "@ember/service";
export default class ChatChannelEditTitleController extends Controller.extend(
ModalFunctionality
) {
@service chatApi;
editedName = "";
@computed("model.title", "editedName")
get isSaveDisabled() {
return (
this.model.title === this.editedName ||
this.editedName?.length > this.siteSettings.max_topic_title_length
);
}
onShow() {
this.set("editedName", this.model.title || "");
}
onClose() {
this.set("editedName", "");
this.clearFlash();
}
@action
onSaveChatChannelName() {
return this.chatApi
.updateChannel(this.model.id, {
name: this.editedName,
})
.then((result) => {
this.model.set("title", result.channel.title);
this.send("closeModal");
})
.catch((event) => {
if (event.jqXHR?.responseJSON?.errors) {
this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}
});
}
@action
onChangeChatChannelName(title) {
this.clearFlash();
this.set("editedName", title);
}
}

View File

@ -8,7 +8,7 @@ export default class ChatChannelInfoAboutController extends Controller.extend(
) {
@action
onEditChatChannelName() {
showModal("chat-channel-edit-name", { model: this.model });
showModal("chat-channel-edit-name-slug", { model: this.model });
}
@action

View File

@ -286,6 +286,7 @@ export default class ChatSubscriptionsManager extends Service {
channel.setProperties({
title: busData.name,
description: busData.description,
slug: busData.slug,
});
}
});

View File

@ -0,0 +1,43 @@
<DModalBody @title="chat.channel_edit_name_slug_modal.title">
<div class="edit-channel-control">
<label for="channel-name" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.name"}}
</label>
<Input
name="channel-name"
class="chat-channel-edit-name-slug-modal__name-input"
placeholder={{i18n "chat.channel_edit_name_slug_modal.input_placeholder"}}
@type="text"
@value={{this.editedName}}
{{on "input" (action "onChangeChatChannelName" value="target.value")}}
/>
</div>
<div class="edit-channel-control">
<label for="channel-slug" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>{{i18n "chat.channel_edit_name_slug_modal.slug_description"}}</DTooltip>
</span>
</label>
<Input
name="channel-slug"
class="chat-channel-edit-name-slug-modal__slug-input"
placeholder={{this.autoGeneratedSlug}}
{{on "input" (action "onChangeChatChannelSlug" value="target.value")}}
@type="text"
@value={{this.editedSlug}}
/>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary create"
@action={{action "onSaveChatChannelName"}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{route-action "closeModal"}} />
</div>

View File

@ -14,7 +14,11 @@
<div class="create-channel-control">
<label for="channel-slug" class="create-channel-label">
{{i18n "chat.create_channel.slug"}}
{{i18n "chat.create_channel.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>{{i18n "chat.channel_edit_name_slug_modal.slug_description"}}</DTooltip>
</span>
</label>
<Input
name="channel-slug"
@ -85,4 +89,4 @@
@label="chat.create_channel.create"
@disabled={{this.createDisabled}}
/>
</div>
</div>

View File

@ -32,6 +32,11 @@
color: var(--primary-medium);
}
.channel-info-about-view__slug {
color: var(--primary-medium);
font-size: var(--font-down-2);
}
.channel-settings-view__desktop-notification-level-selector,
.channel-settings-view__mobile-notification-level-selector,
.channel-settings-view__muted-selector,
@ -117,14 +122,21 @@ input.channel-members-view__search-input {
}
}
// Channel info edit name modal
.chat-channel-edit-name-modal__name-input {
display: flex;
margin: 0;
width: 100%;
// Channel info edit name and slug modal
.chat-channel-edit-name-slug-modal {
.modal-inner-container {
width: 300px;
}
&__name-input,
&__slug-input {
display: flex;
margin: 0;
width: 100%;
}
}
.chat-channel-edit-name-modal__description {
.chat-channel-edit-name-slug-modal__description {
display: flex;
padding: 0.5rem 0;
color: var(--primary-medium);

View File

@ -30,7 +30,8 @@
color: var(--secondary-low);
}
.create-channel-control {
.create-channel-control,
.edit-channel-control {
margin-bottom: 1rem;
}

View File

@ -244,10 +244,12 @@ en:
members: Members
settings: Settings
channel_edit_name_modal:
title: Edit name
channel_edit_name_slug_modal:
title: Edit channel
input_placeholder: Add a name
description: Give a short descriptive name to your channel
slug_description: A channel slug is used in the URL instead of the channel name
name: Channel name
slug: Channel slug (optional)
channel_edit_description_modal:
title: Edit description

View File

@ -431,6 +431,21 @@ RSpec.describe Chat::Api::ChatChannelsController do
end
end
context "when user provides an empty slug" do
fab!(:user) { Fabricate(:admin) }
fab!(:channel) do
Fabricate(:category_channel, name: "something else", description: "something")
end
before { sign_in(user) }
it "does not nullify the slug" do
put "/chat/api/channels/#{channel.id}", params: { channel: { slug: " " } }
expect(channel.reload.slug).to eq("something-else")
end
end
context "when channel is a direct message channel" do
fab!(:user) { Fabricate(:admin) }
fab!(:channel) { Fabricate(:direct_message_channel) }
@ -455,11 +470,13 @@ RSpec.describe Chat::Api::ChatChannelsController do
params: {
channel: {
name: "joffrey",
slug: "cat-king",
description: "cat owner",
},
}
expect(channel.reload.name).to eq("joffrey")
expect(channel.reload.slug).to eq("cat-king")
expect(channel.reload.description).to eq("cat owner")
end
@ -474,7 +491,12 @@ RSpec.describe Chat::Api::ChatChannelsController do
}
end
expect(messages[0].data[:chat_channel_id]).to eq(channel.id)
message = messages[0]
channel.reload
expect(message.data[:chat_channel_id]).to eq(channel.id)
expect(message.data[:name]).to eq(channel.name)
expect(message.data[:slug]).to eq(channel.slug)
expect(message.data[:description]).to eq(channel.description)
end
it "returns a valid chat channel" do

View File

@ -17,6 +17,7 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do
expect(page.find(".category-name")).to have_content(channel_1.chatable.name)
expect(page.find(".channel-info-about-view__name")).to have_content(channel_1.title)
expect(page.find(".channel-info-about-view__slug")).to have_content(channel_1.slug)
end
it "escapes channel title" do
@ -31,10 +32,10 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do
)
end
it "can’t edit name" do
it "can’t edit name or slug" do
chat_page.visit_channel_about(channel_1)
expect(page).to have_no_selector(".edit-name-btn")
expect(page).to have_no_selector(".edit-name-slug-btn")
end
it "can’t edit description" do
@ -78,12 +79,12 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do
it "can edit name" do
chat_page.visit_channel_about(channel_1)
find(".edit-name-btn").click
find(".edit-name-slug-btn").click
expect(find(".chat-channel-edit-name-modal__name-input").value).to eq(channel_1.title)
expect(find(".chat-channel-edit-name-slug-modal__name-input").value).to eq(channel_1.title)
name = "A new name"
find(".chat-channel-edit-name-modal__name-input").fill_in(with: name)
find(".chat-channel-edit-name-slug-modal__name-input").fill_in(with: name)
find(".create").click
expect(page).to have_content(name)
@ -104,5 +105,33 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do
expect(page).to have_content(description)
end
it "can edit slug" do
chat_page.visit_channel_about(channel_1)
find(".edit-name-slug-btn").click
expect(find(".chat-channel-edit-name-slug-modal__slug-input").value).to eq(channel_1.slug)
slug = "gonzo-slug"
find(".chat-channel-edit-name-slug-modal__slug-input").fill_in(with: slug)
find(".create").click
expect(page).to have_content(slug)
end
it "can clear the slug to use the autogenerated version based on the name" do
channel_1.update!(name: "test channel")
chat_page.visit_channel_about(channel_1)
find(".edit-name-slug-btn").click
slug_input = find(".chat-channel-edit-name-slug-modal__slug-input")
expect(slug_input.value).to eq(channel_1.slug)
slug_input.fill_in(with: "")
wait_for_attribute(slug_input, :placeholder, "test-channel")
find(".create").click
expect(page).to have_content("test-channel")
end
end
end