From db5ad345081d97575c06ae8f8f52ca111a8b089f Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Mon, 30 Jan 2023 13:18:34 +1000 Subject: [PATCH] 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. --- .../api/chat_channels_controller.rb | 2 +- plugins/chat/app/services/chat_publisher.rb | 1 + .../components/chat-channel-about-view.hbs | 5 +- .../chat-channel-edit-name-slug.js | 94 +++++++++++++++++++ .../controllers/chat-channel-edit-name.js | 50 ---------- .../controllers/chat-channel-info-about.js | 2 +- .../services/chat-subscriptions-manager.js | 1 + .../modal/chat-channel-edit-name-slug.hbs | 43 +++++++++ .../templates/modal/create-channel.hbs | 8 +- .../stylesheets/common/chat-channel-info.scss | 24 +++-- .../common/create-channel-modal.scss | 3 +- plugins/chat/config/locales/client.en.yml | 8 +- .../api/chat_channels_controller_spec.rb | 24 ++++- .../spec/system/channel_about_page_spec.rb | 39 +++++++- 14 files changed, 233 insertions(+), 71 deletions(-) create mode 100644 plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name-slug.js delete mode 100644 plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js create mode 100644 plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name-slug.hbs diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb index 1a618b96787..bf0618fc01c 100644 --- a/plugins/chat/app/controllers/api/chat_channels_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb @@ -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 diff --git a/plugins/chat/app/services/chat_publisher.rb b/plugins/chat/app/services/chat_publisher.rb index f628cf5b674..e02a52328a3 100644 --- a/plugins/chat/app/services/chat_publisher.rb +++ b/plugins/chat/app/services/chat_publisher.rb @@ -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), ) diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs index 5747a77885c..77f999c9b69 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs @@ -22,7 +22,7 @@ {{#if (chat-guardian "can-edit-chat-channel")}}
@@ -33,6 +33,9 @@
{{replace-emoji this.channel.escapedTitle}}
+
+ {{this.channel.slug}} +
diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name-slug.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name-slug.js new file mode 100644 index 00000000000..be4d23e7279 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name-slug.js @@ -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); + }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js deleted file mode 100644 index fcba0d77118..00000000000 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js +++ /dev/null @@ -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); - } -} diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js index 73514a7bd6d..b21daefbe2c 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js @@ -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 diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js index 4b58c3153b9..42644d9189c 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js @@ -286,6 +286,7 @@ export default class ChatSubscriptionsManager extends Service { channel.setProperties({ title: busData.name, description: busData.description, + slug: busData.slug, }); } }); diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name-slug.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name-slug.hbs new file mode 100644 index 00000000000..54ac92874e1 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name-slug.hbs @@ -0,0 +1,43 @@ + +
+ + +
+ +
+ + +
+
+ + diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs index ae6e068897a..b7a005ab954 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs @@ -14,7 +14,11 @@
-
\ No newline at end of file + diff --git a/plugins/chat/assets/stylesheets/common/chat-channel-info.scss b/plugins/chat/assets/stylesheets/common/chat-channel-info.scss index 091ea9366d8..0c9a4f9a8d7 100644 --- a/plugins/chat/assets/stylesheets/common/chat-channel-info.scss +++ b/plugins/chat/assets/stylesheets/common/chat-channel-info.scss @@ -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); diff --git a/plugins/chat/assets/stylesheets/common/create-channel-modal.scss b/plugins/chat/assets/stylesheets/common/create-channel-modal.scss index bfd8a7735ec..f011e2478bf 100644 --- a/plugins/chat/assets/stylesheets/common/create-channel-modal.scss +++ b/plugins/chat/assets/stylesheets/common/create-channel-modal.scss @@ -30,7 +30,8 @@ color: var(--secondary-low); } - .create-channel-control { + .create-channel-control, + .edit-channel-control { margin-bottom: 1rem; } diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml index bda7d953a38..41a165d0bd0 100644 --- a/plugins/chat/config/locales/client.en.yml +++ b/plugins/chat/config/locales/client.en.yml @@ -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 diff --git a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb index 96d48c05b43..30955457085 100644 --- a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb +++ b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb @@ -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 diff --git a/plugins/chat/spec/system/channel_about_page_spec.rb b/plugins/chat/spec/system/channel_about_page_spec.rb index 192ddd94b6b..567322f774b 100644 --- a/plugins/chat/spec/system/channel_about_page_spec.rb +++ b/plugins/chat/spec/system/channel_about_page_spec.rb @@ -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