mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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:
parent
7ec6e6b3d0
commit
db5ad34508
plugins/chat
app
assets
javascripts/discourse
components
controllers
services
templates/modal
stylesheets/common
config/locales
spec
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -286,6 +286,7 @@ export default class ChatSubscriptionsManager extends Service {
|
||||
channel.setProperties({
|
||||
title: busData.name,
|
||||
description: busData.description,
|
||||
slug: busData.slug,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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"}}
|
||||
<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>
|
@ -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"}}
|
||||
<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>
|
||||
|
@ -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);
|
||||
|
@ -30,7 +30,8 @@
|
||||
color: var(--secondary-low);
|
||||
}
|
||||
|
||||
.create-channel-control {
|
||||
.create-channel-control,
|
||||
.edit-channel-control {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user