DEV: Remove enable_experimental_hashtag_autocomplete logic (#22820)

This commit removes any logic in the app and in specs around
enable_experimental_hashtag_autocomplete and deletes some
old category hashtag code that is no longer necessary.

It also adds a `slug_ref` category instance method, which
will generate a reference like `parent:child` for a category,
with an optional depth, which hashtags use. Also refactors
PostRevisor which was using CategoryHashtagDataSource directly
which is a no-no.

Deletes the old hashtag markdown rule as well.
This commit is contained in:
Martin Brennan 2023-08-08 11:18:55 +10:00 committed by GitHub
parent 0c88bf341a
commit 09223e5ae7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 310 additions and 1001 deletions

View File

@ -14,10 +14,6 @@ import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import {
fetchUnseenHashtags,
linkSeenHashtags,
} from "discourse/lib/link-hashtags";
import {
fetchUnseenHashtagsInContext,
linkSeenHashtagsInContext,
@ -500,24 +496,12 @@ export default Component.extend(
},
_renderUnseenHashtags(preview) {
let unseen;
const hashtagContext = this.site.hashtag_configurations["topic-composer"];
if (this.siteSettings.enable_experimental_hashtag_autocomplete) {
unseen = linkSeenHashtagsInContext(hashtagContext, preview);
} else {
unseen = linkSeenHashtags(preview);
}
const unseen = linkSeenHashtagsInContext(hashtagContext, preview);
if (unseen.length > 0) {
if (this.siteSettings.enable_experimental_hashtag_autocomplete) {
fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => {
linkSeenHashtagsInContext(hashtagContext, preview);
});
} else {
fetchUnseenHashtags(unseen).then(() => {
linkSeenHashtags(preview);
});
}
fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => {
linkSeenHashtagsInContext(hashtagContext, preview);
});
}
},
@ -946,15 +930,9 @@ export default Component.extend(
this._warnCannotSeeMention(preview);
// Paint category, tag, and other data source hashtags
let unseenHashtags;
const hashtagContext =
this.site.hashtag_configurations["topic-composer"];
if (this.siteSettings.enable_experimental_hashtag_autocomplete) {
unseenHashtags = linkSeenHashtagsInContext(hashtagContext, preview);
} else {
unseenHashtags = linkSeenHashtags(preview);
}
if (unseenHashtags.length > 0) {
if (linkSeenHashtagsInContext(hashtagContext, preview).length > 0) {
discourseDebounce(this, this._renderUnseenHashtags, preview, 450);
}

View File

@ -18,13 +18,15 @@ import I18n from "I18n";
import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
import { SKIP } from "discourse/lib/autocomplete";
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
import {
linkSeenHashtagsInContext,
setupHashtagAutocomplete,
} from "discourse/lib/hashtag-autocomplete";
import deprecated from "discourse-common/lib/deprecated";
import discourseDebounce from "discourse-common/lib/debounce";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { getRegister } from "discourse-common/lib/get-owner";
import { isTesting } from "discourse-common/config/environment";
import { linkSeenHashtags } from "discourse/lib/link-hashtags";
import { linkSeenMentions } from "discourse/lib/link-mentions";
import { loadOneboxes } from "discourse/lib/load-oneboxes";
import loadScript from "discourse/lib/load-script";
@ -438,8 +440,10 @@ export default Component.extend(TextareaTextManipulation, {
if (this.siteSettings.enable_diffhtml_preview) {
const cookedElement = document.createElement("div");
cookedElement.innerHTML = cooked;
linkSeenHashtags(cookedElement);
linkSeenHashtagsInContext(
this.site.hashtag_configurations["topic-composer"],
cookedElement
);
linkSeenMentions(cookedElement, this.siteSettings);
resolveCachedShortUrls(this.siteSettings, cookedElement);
loadOneboxes(

View File

@ -5,16 +5,13 @@ export default {
after: "hashtag-css-generator",
initialize(owner) {
const siteSettings = owner.lookup("service:site-settings");
const site = owner.lookup("service:site");
withPluginApi("0.8.7", (api) => {
if (siteSettings.enable_experimental_hashtag_autocomplete) {
api.decorateCookedElement((post) => decorateHashtags(post, site), {
onlyStream: true,
id: "hashtag-icons",
});
}
api.decorateCookedElement((post) => decorateHashtags(post, site), {
onlyStream: true,
id: "hashtag-icons",
});
});
},
};

View File

@ -11,7 +11,6 @@ import {
escapeExpression,
inCodeBlock,
} from "discourse/lib/utilities";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import { emojiUnescape } from "discourse/lib/text";
import { htmlSafe } from "@ember/template";
@ -76,16 +75,12 @@ export function setupHashtagAutocomplete(
siteSettings,
autocompleteOptions = {}
) {
if (siteSettings.enable_experimental_hashtag_autocomplete) {
_setupExperimental(
contextualHashtagConfiguration,
$textArea,
siteSettings,
autocompleteOptions
);
} else {
_setup($textArea, siteSettings, autocompleteOptions.afterComplete);
}
_setup(
contextualHashtagConfiguration,
$textArea,
siteSettings,
autocompleteOptions
);
}
export function hashtagTriggerRule(textarea) {
@ -147,7 +142,7 @@ export function linkSeenHashtagsInContext(
.filter((slug) => !checkedHashtags.has(slug));
}
function _setupExperimental(
function _setup(
contextualHashtagConfiguration,
$textArea,
siteSettings,
@ -171,22 +166,6 @@ function _setupExperimental(
});
}
function _setup($textArea, siteSettings, afterComplete) {
$textArea.autocomplete({
template: findRawTemplate("category-tag-autocomplete"),
key: "#",
afterComplete,
transformComplete: (obj) => obj.text,
dataSource: (term) => {
if (term.match(/\s/)) {
return null;
}
return searchCategoryTag(term, siteSettings);
},
triggerRule: (textarea, opts) => hashtagTriggerRule(textarea, opts),
});
}
let searchCache = {};
let searchCacheTime;
let currentSearch;
@ -220,10 +199,6 @@ function _searchGeneric(term, siteSettings, contextualHashtagConfiguration) {
resolve(CANCELLED_STATUS);
}, 5000);
if (!siteSettings.enable_experimental_hashtag_autocomplete && term === "") {
return resolve(CANCELLED_STATUS);
}
const debouncedSearch = (q, ctx, resultFunc) => {
discourseDebounce(this, _searchRequest, q, ctx, resultFunc, INPUT_DELAY);
};

View File

@ -1,3 +1,6 @@
// TODO (martin) Delete this after core PR and any other PRs that depend
// on this file (e.g. discourse-encrypt) are merged.
import deprecated from "discourse-common/lib/deprecated";
import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
import { ajax } from "discourse/lib/ajax";

View File

@ -9,7 +9,6 @@ acceptance("#hashtag autocompletion in composer", function (needs) {
needs.user();
needs.settings({
tagging_enabled: true,
enable_experimental_hashtag_autocomplete: true,
});
needs.pretender((server, helper) => {
server.get("/hashtags", () => {

View File

@ -1,51 +0,0 @@
import {
acceptance,
query,
visible,
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
acceptance("Category and Tag Hashtags", function (needs) {
needs.user();
needs.settings({
tagging_enabled: true,
enable_experimental_hashtag_autocomplete: false,
});
needs.pretender((server, helper) => {
server.get("/hashtags", () => {
return helper.response({
categories: { bug: "/c/bugs" },
tags: {
monkey: "/tag/monkey",
bug: "/tag/bug",
},
});
});
});
test("hashtags are cooked properly", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .btn.create");
await fillIn(
".d-editor-input",
`this is a category hashtag #bug
this is a tag hashtag #monkey
category vs tag: #bug vs #bug::tag
uppercase hashtag works too #BUG, #BUG::tag`
);
assert.ok(visible(".d-editor-preview"));
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
`<p>this is a category hashtag <a href="/c/bugs" class="hashtag" tabindex=\"-1\">#<span>bug</span></a></p>
<p>this is a tag hashtag <a href="/tag/monkey" class="hashtag" tabindex=\"-1\">#<span>monkey</span></a></p>
<p>category vs tag: <a href="/c/bugs" class="hashtag" tabindex=\"-1\">#<span>bug</span></a> vs <a href="/tag/bug" class="hashtag" tabindex=\"-1\">#<span>bug</span></a></p>
<p>uppercase hashtag works too <a href="/c/bugs" class="hashtag" tabindex=\"-1\">#<span>BUG</span></a>, <a href="/tag/bug" class="hashtag" tabindex=\"-1\">#<span>BUG</span></a></p>`
);
});
});

View File

@ -643,73 +643,6 @@ eviltrout</p>
);
});
test("Category hashtags", function (assert) {
const alwaysTrue = {
categoryHashtagLookup: function () {
return [
"http://test.discourse.org/category-hashtag",
"category-hashtag",
];
},
};
assert.cookedOptions(
"Check out #category-hashtag",
alwaysTrue,
'<p>Check out <a class="hashtag" href="http://test.discourse.org/category-hashtag">#<span>category-hashtag</span></a></p>',
"it translates category hashtag into links"
);
assert.cooked(
"Check out #category-hashtag",
'<p>Check out <span class="hashtag">#category-hashtag</span></p>',
"it does not translate category hashtag into links if it is not a valid category hashtag"
);
assert.cookedOptions(
"[#category-hashtag](http://www.test.com)",
alwaysTrue,
'<p><a href="http://www.test.com">#category-hashtag</a></p>',
"it does not translate category hashtag within links"
);
assert.cooked(
"```\n# #category-hashtag\n```",
'<pre><code class="lang-auto"># #category-hashtag\n</code></pre>',
"it does not translate category hashtags to links in code blocks"
);
assert.cooked(
"># #category-hashtag\n",
'<blockquote>\n<h1><span class="hashtag">#category-hashtag</span></h1>\n</blockquote>',
"it handles category hashtags in simple quotes"
);
assert.cooked(
"# #category-hashtag",
'<h1><a name="category-hashtag-1" class="anchor" href="#category-hashtag-1"></a><span class="hashtag">#category-hashtag</span></h1>',
"it works within ATX-style headers"
);
assert.cooked(
"don't `#category-hashtag`",
"<p>don't <code>#category-hashtag</code></p>",
"it does not mention in an inline code block"
);
assert.cooked(
"<small>#category-hashtag</small>",
'<p><small><span class="hashtag">#category-hashtag</span></small></p>',
"it works between HTML tags"
);
assert.cooked(
"Checkout #ụdị",
'<p>Checkout <span class="hashtag">#ụdị</span></p>',
"it works for non-english characters"
);
});
test("Heading", function (assert) {
assert.cooked(
"**Bold**\n----------",

View File

@ -547,8 +547,6 @@ export function setup(opts, siteSettings, state) {
markdownTypographerQuotationMarks:
siteSettings.markdown_typographer_quotation_marks,
markdownLinkifyTlds: siteSettings.markdown_linkify_tlds,
enableExperimentalHashtagAutocomplete:
siteSettings.enable_experimental_hashtag_autocomplete,
};
const markdownitOpts = {

View File

@ -28,7 +28,6 @@ export function buildOptions(state) {
getTopicInfo,
topicId,
forceQuoteLink,
categoryHashtagLookup,
userId,
getCurrentUser,
currentUser,
@ -67,7 +66,6 @@ export function buildOptions(state) {
getTopicInfo,
topicId,
forceQuoteLink,
categoryHashtagLookup,
userId,
getCurrentUser,
currentUser,

View File

@ -1,65 +0,0 @@
// TODO (martin) Remove this once enable_experimental_hashtag_autocomplete
// (and by extension enableExperimentalHashtagAutocomplete) is not required
// anymore, the new hashtag-autocomplete rule replaces it.
function addHashtag(buffer, matches, state) {
const options = state.md.options.discourse;
const slug = matches[1];
const categoryHashtagLookup = options.categoryHashtagLookup;
const result = categoryHashtagLookup && categoryHashtagLookup(slug);
let token;
if (result) {
token = new state.Token("link_open", "a", 1);
token.attrs = [
["class", "hashtag"],
["href", result[0]],
];
token.block = false;
buffer.push(token);
token = new state.Token("text", "", 0);
token.content = "#";
buffer.push(token);
token = new state.Token("span_open", "span", 1);
token.block = false;
buffer.push(token);
token = new state.Token("text", "", 0);
token.content = result[1];
buffer.push(token);
buffer.push(new state.Token("span_close", "span", -1));
buffer.push(new state.Token("link_close", "a", -1));
} else {
token = new state.Token("span_open", "span", 1);
token.attrs = [["class", "hashtag"]];
buffer.push(token);
token = new state.Token("text", "", 0);
token.content = matches[0];
buffer.push(token);
token = new state.Token("span_close", "span", -1);
buffer.push(token);
}
}
export function setup(helper) {
helper.registerPlugin((md) => {
if (
!md.options.discourse.limitedSiteSettings
.enableExperimentalHashtagAutocomplete
) {
const rule = {
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/,
onMatch: addHashtag,
};
md.core.textPostProcess.ruler.push("category-hashtag", rule);
}
});
}

View File

@ -87,17 +87,12 @@ function addIconPlaceholder(buffer, state) {
export function setup(helper) {
helper.registerPlugin((md) => {
if (
md.options.discourse.limitedSiteSettings
.enableExperimentalHashtagAutocomplete
) {
const rule = {
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/,
onMatch: addHashtag,
};
const rule = {
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/,
onMatch: addHashtag,
};
md.core.textPostProcess.ruler.push("hashtag-autocomplete", rule);
}
md.core.textPostProcess.ruler.push("hashtag-autocomplete", rule);
});
helper.allowList([

View File

@ -4,11 +4,7 @@ class HashtagsController < ApplicationController
requires_login
def lookup
if SiteSetting.enable_experimental_hashtag_autocomplete
render json: HashtagAutocompleteService.new(guardian).lookup(params[:slugs], params[:order])
else
render json: HashtagAutocompleteService.new(guardian).lookup_old(params[:slugs])
end
render json: HashtagAutocompleteService.new(guardian).lookup(params[:slugs], params[:order])
end
def search

View File

@ -18,6 +18,8 @@ class Category < ActiveRecord::Base
REQUIRE_TOPIC_APPROVAL = "require_topic_approval"
REQUIRE_REPLY_APPROVAL = "require_reply_approval"
NUM_AUTO_BUMP_DAILY = "num_auto_bump_daily"
SLUG_REF_SEPARATOR = ":"
register_custom_field_type(REQUIRE_TOPIC_APPROVAL, :boolean)
register_custom_field_type(REQUIRE_REPLY_APPROVAL, :boolean)
@ -1037,6 +1039,20 @@ class Category < ActiveRecord::Base
end
end
def slug_ref(depth: 1)
if self.parent_category_id.present?
built_ref = [self.slug]
parent = self.parent_category
while parent.present? && (built_ref.length < depth + 1)
built_ref << parent.slug
parent = parent.parent_category
end
built_ref.reverse.join(Category::SLUG_REF_SEPARATOR)
else
self.slug
end
end
def cannot_delete_reason
return I18n.t("category.cannot_delete.uncategorized") if self.uncategorized?
return I18n.t("category.cannot_delete.has_subcategories") if self.has_children?

View File

@ -3,26 +3,7 @@
module CategoryHashtag
extend ActiveSupport::Concern
SEPARATOR = ":"
class_methods do
# TODO (martin) Remove this when enable_experimental_hashtag_autocomplete
# becomes the norm, it is reimplemented below for CategoryHashtagDataSourcee
def query_from_hashtag_slug(category_slug)
slug_path = split_slug_path(category_slug)
return if slug_path.blank?
slug_path.map! { |slug| CGI.escape(slug) } if SiteSetting.slug_generation_method == "encoded"
parent_slug, child_slug = slug_path.last(2)
categories = Category.where(slug: parent_slug)
if child_slug
Category.where(slug: child_slug, parent_category_id: categories.select(:id)).first
else
categories.where(parent_category_id: nil).first
end
end
##
# Finds any categories that match the provided slugs, supporting
# the parent:child format for category slugs (only one level of
@ -66,7 +47,7 @@ module CategoryHashtag
end
def split_slug_path(slug)
slug_path = slug.split(SEPARATOR)
slug_path = slug.split(Category::SLUG_REF_SEPARATOR)
return if slug_path.empty? || slug_path.size > 2
slug_path
end

View File

@ -5,7 +5,7 @@
# categories via the # autocomplete character.
class CategoryHashtagDataSource
def self.enabled?
SiteSetting.enable_experimental_hashtag_autocomplete
true
end
def self.icon
@ -27,12 +27,7 @@ class CategoryHashtagDataSource
# Single-level category heirarchy should be enough to distinguish between
# categories here.
item.ref =
if category.parent_category_id
"#{category.parent_category.slug}:#{category.slug}"
else
category.slug
end
item.ref = category.slug_ref
end
end

View File

@ -299,51 +299,6 @@ class HashtagAutocompleteService
append_types_to_conflicts(limited_results, top_ranked_type, types_in_priority_order, limit)
end
# TODO (martin) Remove this once plugins are not relying on the old lookup
# behavior via HashtagsController when enable_experimental_hashtag_autocomplete is removed
def lookup_old(slugs)
raise Discourse::InvalidParameters.new(:slugs) if !slugs.is_a?(Array)
all_slugs = []
tag_slugs = []
slugs[0..HashtagAutocompleteService::HASHTAGS_PER_REQUEST].each do |slug|
if slug.end_with?(PrettyText::Helpers::TAG_HASHTAG_POSTFIX)
tag_slugs << slug.chomp(PrettyText::Helpers::TAG_HASHTAG_POSTFIX)
else
all_slugs << slug
end
end
# Try to resolve hashtags as categories first
category_slugs_and_ids =
all_slugs.map { |slug| [slug, Category.query_from_hashtag_slug(slug)&.id] }.to_h
category_ids_and_urls =
Category
.secured(guardian)
.select(:id, :slug, :parent_category_id) # fields required for generating category URL
.where(id: category_slugs_and_ids.values)
.map { |c| [c.id, c.url] }
.to_h
categories_hashtags = {}
category_slugs_and_ids.each do |slug, id|
if category_url = category_ids_and_urls[id]
categories_hashtags[slug] = category_url
end
end
# Resolve remaining hashtags as tags
tag_hashtags = {}
if SiteSetting.tagging_enabled
tag_slugs += (all_slugs - categories_hashtags.keys)
DiscourseTagging
.filter_visible(Tag.where_name(tag_slugs), guardian)
.each { |tag| tag_hashtags[tag.name] = tag.full_url }
end
{ categories: categories_hashtags, tags: tag_hashtags }
end
private
def search_using_condition(limited_results, term, type, limit, condition)

View File

@ -5,7 +5,7 @@
# tags via the # autocomplete character.
class TagHashtagDataSource
def self.enabled?
SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.tagging_enabled
SiteSetting.tagging_enabled
end
def self.icon

View File

@ -160,18 +160,14 @@ class PostRevisor
user,
I18n.t(
"topic_category_changed",
from: category_name_raw(old_category),
to: category_name_raw(new_category),
from: "##{old_category.slug_ref}",
to: "##{new_category.slug_ref}",
),
post_type: Post.types[:small_action],
action_code: "category_changed",
)
end
def self.category_name_raw(category)
"##{CategoryHashtagDataSource.category_to_hashtag_item(category).ref}"
end
def self.create_small_action_for_tag_changes(topic:, user:, added_tags:, removed_tags:)
return if !SiteSetting.create_post_for_category_and_tag_changes

View File

@ -197,7 +197,6 @@ module PrettyText
__optInput.lookupPrimaryUserGroup = __lookupPrimaryUserGroup;
__optInput.formatUsername = __formatUsername;
__optInput.getTopicInfo = __getTopicInfo;
__optInput.categoryHashtagLookup = __categoryLookup;
__optInput.hashtagLookup = __hashtagLookup;
__optInput.customEmoji = #{custom_emoji.to_json};
__optInput.customEmojiTranslation = #{Plugin::CustomEmoji.translations.to_json};
@ -471,10 +470,7 @@ module PrettyText
DiscourseEvent.trigger(:reduce_excerpt, doc, options)
strip_image_wrapping(doc)
strip_oneboxed_media(doc)
if SiteSetting.enable_experimental_hashtag_autocomplete && options[:plain_hashtags]
convert_hashtag_links_to_plaintext(doc)
end
convert_hashtag_links_to_plaintext(doc) if options[:plain_hashtags]
html = doc.to_html
ExcerptParser.get_excerpt(html, max_length, options)

View File

@ -96,21 +96,6 @@ module PrettyText
end
end
# TODO (martin) Remove this when everything is using hashtag_lookup
# after enable_experimental_hashtag_autocomplete is default.
def category_tag_hashtag_lookup(text)
is_tag = text =~ /#{TAG_HASHTAG_POSTFIX}\z/
if !is_tag && category = Category.query_from_hashtag_slug(text)
[category.url, text]
elsif (!is_tag && tag = Tag.find_by(name: text)) ||
(is_tag && tag = Tag.find_by(name: text.gsub!(TAG_HASHTAG_POSTFIX, "")))
[tag.url, text]
else
nil
end
end
def hashtag_lookup(slug, cooking_user_id, types_in_priority_order)
# NOTE: This is _somewhat_ expected since we need to be able to cook posts
# etc. without a user sometimes, but it is still an edge case.

View File

@ -106,12 +106,6 @@ function __getTopicInfo(i) {
return __helpers.get_topic_info(i);
}
// TODO (martin) Remove this when everything is using hashtag_lookup
// after enable_experimental_hashtag_autocomplete is default.
function __categoryLookup(c) {
return __helpers.category_tag_hashtag_lookup(c);
}
function __hashtagLookup(slug, cookingUserId, typesInPriorityOrder) {
return __helpers.hashtag_lookup(slug, cookingUserId, typesInPriorityOrder);
}

View File

@ -301,9 +301,7 @@ module Chat
end
def channel_hashtag_or_name
if chat_channel.slug.present? && SiteSetting.enable_experimental_hashtag_autocomplete
return "##{chat_channel.slug}::channel"
end
return "##{chat_channel.slug}::channel" if chat_channel.slug.present?
chat_channel_title
end
end

View File

@ -3,7 +3,7 @@
module Chat
class ChannelHashtagDataSource
def self.enabled?
SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.enable_public_channels
SiteSetting.enable_public_channels
end
def self.icon

View File

@ -186,28 +186,24 @@ describe Chat::ChannelArchiveService do
expect(pm_topic.first_post.raw).to include("Title can't have more than 1 emoji")
end
context "when enable_experimental_hashtag_autocomplete" do
before { SiteSetting.enable_experimental_hashtag_autocomplete = true }
it "uses the channel slug to autolink a hashtag for the channel in the PM" do
create_messages(3) && start_archive
described_class.new(@channel_archive).execute
expect(@channel_archive.reload.complete?).to eq(true)
pm_topic = Topic.private_messages.last
expect(pm_topic.first_post.cooked).to have_tag(
"a",
with: {
class: "hashtag-cooked",
href: channel.relative_url,
"data-type": "channel",
"data-slug": channel.slug,
"data-id": channel.id,
"data-ref": "#{channel.slug}::channel",
},
) do
with_tag("span", with: { class: "hashtag-icon-placeholder" })
with_tag("span", text: channel.title(user))
end
it "uses the channel slug to autolink a hashtag for the channel in the PM" do
create_messages(3) && start_archive
described_class.new(@channel_archive).execute
expect(@channel_archive.reload.complete?).to eq(true)
pm_topic = Topic.private_messages.last
expect(pm_topic.first_post.cooked).to have_tag(
"a",
with: {
class: "hashtag-cooked",
href: channel.relative_url,
"data-type": "channel",
"data-slug": channel.slug,
"data-id": channel.id,
"data-ref": "#{channel.slug}::channel",
},
) do
with_tag("span", with: { class: "hashtag-icon-placeholder" })
with_tag("span", text: channel.title(user))
end
end

View File

@ -27,7 +27,6 @@ RSpec.describe Chat::ChannelHashtagDataSource do
let!(:guardian) { Guardian.new(user) }
before do
SiteSetting.enable_experimental_hashtag_autocomplete = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_1]
Group.refresh_automatic_groups!
end

View File

@ -228,22 +228,8 @@ describe Chat::Message do
expect(cooked).to eq("<p><span class=\"mention\">@mention</span></p>")
end
it "supports category-hashtag plugin" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
category = Fabricate(:category)
cooked = described_class.cook("##{category.slug}")
expect(cooked).to eq(
"<p><a class=\"hashtag\" href=\"#{category.url}\">#<span>#{category.slug}</span></a></p>",
)
end
it "supports hashtag autocomplete" do
SiteSetting.chat_enabled = true
SiteSetting.enable_experimental_hashtag_autocomplete = true
category = Fabricate(:category)
user = Fabricate(:user)
@ -504,7 +490,6 @@ describe Chat::Message do
before do
SiteSetting.chat_enabled = true
SiteSetting.enable_experimental_hashtag_autocomplete = true
SiteSetting.suppress_secured_categories_from_admin = true
end

View File

@ -15,8 +15,6 @@ describe "Using #hashtag autocompletion to search for and lookup channels", type
let(:topic_page) { PageObjects::Pages::Topic.new }
before do
SiteSetting.enable_experimental_hashtag_autocomplete = true
chat_system_bootstrap(user, [channel1, channel2])
sign_in(user)
end

View File

@ -400,18 +400,8 @@ en:
instructions: |-
Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category?
Type `#` in the middle of a sentence and select any category or tag.
instructions_experimental: |-
Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category?
Type `#` anywhere in a sentence and select any category or tag.
not_found: |-
Hmm, I dont see a category in there anywhere. Note that `#` can't be the first character. Can you copy this in your next reply?
```text
I can create a category link via #
```
not_found_experimental: |-
Hmm, I dont see a category in there anywhere. Can you copy this in your next reply?
```text

View File

@ -39,27 +39,11 @@ module DiscourseNarrativeBot
next_state: :tutorial_category_hashtag,
next_instructions:
Proc.new do
category = Category.secured(Guardian.new(@user)).last
slug = category.slug
if parent_category = category.parent_category
slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}"
end
# TODO (martin) When enable_experimental_hashtag_autocomplete is the only option
# update the instructions and remove instructions_experimental, as well as the
# not_found translation
if SiteSetting.enable_experimental_hashtag_autocomplete
I18n.t(
"#{I18N_KEY}.category_hashtag.instructions_experimental",
i18n_post_args(category: "##{slug}"),
)
else
I18n.t(
"#{I18N_KEY}.category_hashtag.instructions",
i18n_post_args(category: "##{slug}"),
)
end
category = Category.secured(@user.guardian).last
I18n.t(
"#{I18N_KEY}.category_hashtag.instructions",
i18n_post_args(category: "##{category.slug_ref}"),
)
end,
recover: {
action: :reply_to_recover,
@ -298,9 +282,7 @@ module DiscourseNarrativeBot
topic_id = @post.topic_id
return unless valid_topic?(topic_id)
hashtag_css_class =
SiteSetting.enable_experimental_hashtag_autocomplete ? ".hashtag-cooked" : ".hashtag"
if Nokogiri::HTML5.fragment(@post.cooked).css(hashtag_css_class).size > 0
if Nokogiri::HTML5.fragment(@post.cooked).css(".hashtag-cooked").size > 0
raw = <<~MD
#{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)}
@ -312,14 +294,7 @@ module DiscourseNarrativeBot
else
fake_delay
unless @data[:attempted]
if SiteSetting.enable_experimental_hashtag_autocomplete
reply_to(
@post,
I18n.t("#{I18N_KEY}.category_hashtag.not_found_experimental", i18n_post_args),
)
else
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args))
end
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args))
end
enqueue_timeout_job(@user)
false

View File

@ -377,9 +377,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
context "when reply contains the skip trigger" do
it "should create the right reply" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
parent_category = Fabricate(:category, name: "a")
_category = Fabricate(:category, parent_category: parent_category, name: "b")
@ -417,9 +414,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
context "when user recovers a post in the right topic" do
it "should create the right reply" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
parent_category = Fabricate(:category, name: "a")
_category = Fabricate(:category, parent_category: parent_category, name: "b")
post
@ -448,9 +442,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
topic_id: topic.id,
track: described_class.to_s,
)
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
end
context "when post is not in the right topic" do
@ -509,37 +500,15 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
narrative.input(:reply, user, post: post)
expected_raw = <<~RAW
#{I18n.t("discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply", base_uri: "")}
#{I18n.t("discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions", base_uri: "")}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(
:tutorial_change_topic_notification_level,
)
end
context "when enable_experimental_hashtag_autocomplete is true" do
before { SiteSetting.enable_experimental_hashtag_autocomplete = true }
it "should create the right reply" do
category = Fabricate(:category)
post.update!(raw: "Check out this ##{category.slug}")
narrative.input(:reply, user, post: post)
expected_raw = <<~RAW
#{I18n.t("discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply", base_uri: "")}
#{I18n.t("discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions", base_uri: "")}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(
:tutorial_change_topic_notification_level,
)
end
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(
:tutorial_change_topic_notification_level,
)
end
end

View File

@ -5,9 +5,6 @@ RSpec.describe "CommonMark" do
SiteSetting.enable_markdown_typographer = false
SiteSetting.highlighted_languages = "ruby|aa"
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
html, state, md = nil
failed = 0
@ -34,7 +31,7 @@ RSpec.describe "CommonMark" do
cooked = PrettyText.markdown(md, sanitize: false)
cooked.strip!
cooked.gsub!(" class=\"lang-auto\"", "")
cooked.gsub!(%r{<span class="hashtag">(.*)</span>}, "\\1")
cooked.gsub!(%r{<span class="hashtag-raw">(.*)</span>}, "\\1")
cooked.gsub!(%r{<a name="(.*)" class="anchor" href="#\1*"></a>}, "")
# we support data-attributes which is not in the spec
cooked.gsub!("<pre data-code-startline=\"3\">", "<pre>")

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
RSpec.describe CategoryHashtag do
describe "#query_from_hashtag_slug" do
fab!(:parent_category) { Fabricate(:category) }
fab!(:child_category) { Fabricate(:category, parent_category: parent_category) }
it "should return the right result for a parent category slug" do
expect(Category.query_from_hashtag_slug(parent_category.slug)).to eq(parent_category)
end
it "should return the right result for a parent and child category slug" do
expect(
Category.query_from_hashtag_slug(
"#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{child_category.slug}",
),
).to eq(child_category)
end
it "should return nil for incorrect parent category slug" do
expect(Category.query_from_hashtag_slug("random-slug")).to eq(nil)
end
it "should return nil for incorrect parent and child category slug" do
expect(
Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug"),
).to eq(nil)
end
it "should return nil for a non-existent root and a parent subcategory" do
expect(
Category.query_from_hashtag_slug(
"non-existent#{CategoryHashtag::SEPARATOR}#{parent_category.slug}",
),
).to eq(nil)
end
context "with multi-level categories" do
before { SiteSetting.max_category_nesting = 3 }
it "should return the right result for a grand child category slug" do
category = Fabricate(:category, parent_category: child_category)
expect(
Category.query_from_hashtag_slug(
"#{child_category.slug}#{CategoryHashtag::SEPARATOR}#{category.slug}",
),
).to eq(category)
end
end
end
end

View File

@ -566,7 +566,6 @@ RSpec.describe Email::Sender do
end
it "changes the hashtags to the slug with a # symbol beforehand rather than the full name of the resource" do
SiteSetting.enable_experimental_hashtag_autocomplete = true
category = Fabricate(:category, slug: "dev")
reply.update!(raw: reply.raw + "\n wow this is #dev")
reply.rebake!

View File

@ -179,7 +179,6 @@ RSpec.describe Oneboxer do
end
it "includes hashtag HTML" do
SiteSetting.enable_experimental_hashtag_autocomplete = true
category = Fabricate(:category, slug: "random")
tag = Fabricate(:tag, name: "bug")
public_post = Fabricate(:post, raw: "This post has some hashtags, #random and #bug")

View File

@ -16,37 +16,6 @@ RSpec.describe PrettyText::Helpers do
end
end
describe ".category_tag_hashtag_lookup" do
fab!(:tag) { Fabricate(:tag, name: "somecooltag") }
fab!(:category) do
Fabricate(:category, name: "Some Awesome Category", slug: "someawesomecategory")
end
it "handles tags based on slug with TAG_HASHTAG_POSTFIX" do
expect(
PrettyText::Helpers.category_tag_hashtag_lookup(
+"somecooltag#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}",
),
).to eq([tag.url, "somecooltag"])
end
it "handles categories based on slug" do
expect(PrettyText::Helpers.category_tag_hashtag_lookup("someawesomecategory")).to eq(
[category.url, "someawesomecategory"],
)
end
it "handles tags based on slug without TAG_HASHTAG_POSTFIX" do
expect(PrettyText::Helpers.category_tag_hashtag_lookup(+"somecooltag")).to eq(
[tag.url, "somecooltag"],
)
end
it "returns nil when no tag or category that matches exists" do
expect(PrettyText::Helpers.category_tag_hashtag_lookup("blah")).to eq(nil)
end
end
describe ".hashtag_lookup" do
fab!(:tag) { Fabricate(:tag, name: "somecooltag", description: "Coolest things ever") }
fab!(:category) do

View File

@ -1722,50 +1722,6 @@ RSpec.describe PrettyText do
end
it "produces hashtag links" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
category = Fabricate(:category, name: "testing")
category2 = Fabricate(:category, name: "known")
Fabricate(:topic, tags: [Fabricate(:tag, name: "known")])
cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing")
[
"<span class=\"hashtag\">#unknown::tag</span>",
"<a class=\"hashtag\" href=\"#{category2.url}\">#<span>known</span></a>",
"<a class=\"hashtag\" href=\"/tag/known\">#<span>known</span></a>",
"<a class=\"hashtag\" href=\"#{category.url}\">#<span>testing</span></a>",
].each { |element| expect(cooked).to include(element) }
cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)")
html = <<~HTML
<p><a href="http://example.com" rel="noopener nofollow ugc"><code>a</code> #known::tag here</a></p>
HTML
expect(cooked).to eq(html.strip)
cooked = PrettyText.cook("<a href='http://example.com'>`a` #known::tag here</a>")
expect(cooked).to eq(html.strip)
cooked = PrettyText.cook("<A href='/a'>test</A> #known::tag")
html = <<~HTML
<p><a href="/a">test</a> <a class="hashtag" href="/tag/known">#<span>known</span></a></p>
HTML
expect(cooked).to eq(html.strip)
# ensure it does not fight with the autolinker
expect(PrettyText.cook(" http://somewhere.com/#known")).not_to include("hashtag")
expect(PrettyText.cook(" http://somewhere.com/?#known")).not_to include("hashtag")
expect(PrettyText.cook(" http://somewhere.com/?abc#known")).not_to include("hashtag")
end
it "produces hashtag links when enable_experimental_hashtag_autocomplete is enabled" do
SiteSetting.enable_experimental_hashtag_autocomplete = true
user = Fabricate(:user)
category = Fabricate(:category, name: "testing", slug: "testing")
category2 = Fabricate(:category, name: "known", slug: "known")
@ -2059,11 +2015,8 @@ HTML
end
it "does not replace hashtags and mentions" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
Fabricate(:user, username: "test")
category = Fabricate(:category, slug: "test")
category = Fabricate(:category, slug: "test", name: "test")
Fabricate(
:watched_word,
action: WatchedWord.actions[:replace],
@ -2071,19 +2024,25 @@ HTML
replacement: "discourse",
)
expect(PrettyText.cook("@test #test test")).to match_html(<<~HTML)
<p>
<a class="mention" href="/u/test">@test</a>
<a class="hashtag" href="/c/test/#{category.id}">#<span>test</span></a>
discourse
</p>
HTML
cooked = PrettyText.cook("@test #test test")
expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" })
expect(cooked).to have_tag(
"a",
text: "test",
with: {
class: "hashtag-cooked",
href: "/c/test/#{category.id}",
"data-type": "category",
"data-slug": category.slug,
"data-id": category.id,
},
) do
with_tag("span", with: { class: "hashtag-icon-placeholder" })
end
expect(cooked).to include("discourse")
end
it "does not replace hashtags and mentions when watched words are regular expressions" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
SiteSetting.enable_experimental_hashtag_autocomplete = false
SiteSetting.watched_words_regular_expressions = true
Fabricate(:user, username: "test")
@ -2095,19 +2054,6 @@ HTML
replacement: "discourse",
)
cooked = PrettyText.cook("@test #test test")
expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" })
expect(cooked).to have_tag(
"a",
text: "#test",
with: {
class: "hashtag",
href: "/c/test/#{category.id}",
},
)
expect(cooked).to include("tdiscourset")
SiteSetting.enable_experimental_hashtag_autocomplete = true
cooked = PrettyText.cook("@test #test test")
expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" })
expect(cooked).to have_tag(

View File

@ -1451,4 +1451,37 @@ RSpec.describe Category do
end
end
end
describe "#slug_ref" do
fab!(:category) { Fabricate(:category, slug: "foo") }
it "returns the slug for categories without parents" do
expect(category.slug_ref).to eq("foo")
end
context "for category with parent" do
fab!(:subcategory) { Fabricate(:category, parent_category: category, slug: "bar") }
it "returns the parent and child slug ref with separator" do
expect(subcategory.slug_ref).to eq("foo#{Category::SLUG_REF_SEPARATOR}bar")
end
end
context "for category with multiple parents" do
let(:subcategory_1) { Fabricate(:category, parent_category: category, slug: "bar") }
let(:subcategory_2) { Fabricate(:category, parent_category: subcategory_1, slug: "boo") }
before { SiteSetting.max_category_nesting = 3 }
it "returns the parent and child slug ref with separator" do
expect(subcategory_2.slug_ref(depth: 2)).to eq(
"foo#{Category::SLUG_REF_SEPARATOR}bar#{Category::SLUG_REF_SEPARATOR}boo",
)
end
it "allows limiting depth" do
expect(subcategory_2.slug_ref(depth: 1)).to eq("bar#{Category::SLUG_REF_SEPARATOR}boo")
end
end
end
end

View File

@ -20,305 +20,184 @@ RSpec.describe HashtagsController do
end
describe "#lookup" do
context "when enable_experimental_hashtag_autocomplete disabled" do
# TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
before { SiteSetting.enable_experimental_hashtag_autocomplete = false }
context "when logged in" do
context "as regular user" do
before { sign_in(Fabricate(:user)) }
context "when logged in" do
context "as regular user" do
before { sign_in(Fabricate(:user)) }
it "returns only valid categories and tags" do
get "/hashtags.json",
params: {
slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name],
}
it "returns only valid categories and tags" do
get "/hashtags.json",
params: {
slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name],
order: %w[category tag],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
"categories" => {
category.slug => category.url,
},
"tags" => {
tag.name => tag.full_url,
},
)
end
it "handles tags with the TAG_HASHTAG_POSTFIX" do
get "/hashtags.json",
params: {
slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
"categories" => {
},
"tags" => {
tag.name => tag.full_url,
},
)
end
it "does not return restricted categories or hidden tags" do
get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] }
expect(response.status).to eq(200)
expect(response.parsed_body).to eq("categories" => {}, "tags" => {})
end
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [
{
"relative_url" => category.url,
"text" => category.name,
"description" => nil,
"icon" => "folder",
"type" => "category",
"ref" => category.slug,
"slug" => category.slug,
"id" => category.id,
},
],
"tag" => [
{
"relative_url" => tag.url,
"text" => tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => tag.name,
"slug" => tag.name,
"secondary_text" => "x0",
"id" => tag.id,
},
],
},
)
end
context "as admin" do
fab!(:admin) { Fabricate(:admin) }
it "handles tags with the ::tag type suffix" do
get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] }
before { sign_in(admin) }
it "returns restricted categories and hidden tags" do
group.add(admin)
get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] }
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
"categories" => {
private_category.slug => private_category.url,
},
"tags" => {
hidden_tag.name => hidden_tag.full_url,
},
)
end
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [],
"tag" => [
{
"relative_url" => tag.url,
"text" => tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => "#{tag.name}::tag",
"slug" => tag.name,
"secondary_text" => "x0",
"id" => tag.id,
},
],
},
)
end
context "with sub-sub-categories" do
before do
SiteSetting.max_category_nesting = 3
sign_in(Fabricate(:user))
end
it "does not return restricted categories or hidden tags" do
get "/hashtags.json",
params: {
slugs: [private_category.slug, hidden_tag.name],
order: %w[category tag],
}
it "works" do
foo = Fabricate(:category_with_definition, slug: "foo")
foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id)
foobarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id)
qux = Fabricate(:category_with_definition, slug: "qux")
quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id)
quxbarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id)
invalid_slugs = [":"]
child_slugs = %w[bar baz]
deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz]
get "/hashtags.json",
params: {
slugs:
invalid_slugs + child_slugs + deeply_nested_slugs +
%w[foo foo:bar bar:baz qux qux:bar],
}
expect(response.status).to eq(200)
expect(response.parsed_body["categories"]).to eq(
"foo" => foo.url,
"foo:bar" => foobar.url,
"bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url,
"qux" => qux.url,
"qux:bar" => quxbar.url,
)
end
expect(response.status).to eq(200)
expect(response.parsed_body).to eq({ "category" => [], "tag" => [] })
end
end
context "when not logged in" do
it "returns invalid access" do
get "/hashtags.json", params: { slugs: [] }
expect(response.status).to eq(403)
context "as admin" do
fab!(:admin) { Fabricate(:admin) }
before { sign_in(admin) }
it "returns restricted categories and hidden tags" do
group.add(admin)
get "/hashtags.json",
params: {
slugs: [private_category.slug, hidden_tag.name],
order: %w[category tag],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [
{
"relative_url" => private_category.url,
"text" => private_category.name,
"description" => nil,
"icon" => "folder",
"type" => "category",
"ref" => private_category.slug,
"slug" => private_category.slug,
"id" => private_category.id,
},
],
"tag" => [
{
"relative_url" => hidden_tag.url,
"text" => hidden_tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => hidden_tag.name,
"slug" => hidden_tag.name,
"secondary_text" => "x0",
"id" => hidden_tag.id,
},
],
},
)
end
end
context "with sub-sub-categories" do
before do
SiteSetting.max_category_nesting = 3
sign_in(Fabricate(:user))
end
it "works" do
foo = Fabricate(:category_with_definition, slug: "foo")
foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id)
foobarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id)
qux = Fabricate(:category_with_definition, slug: "qux")
quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id)
quxbarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id)
invalid_slugs = [":"]
child_slugs = %w[bar baz]
deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz]
get "/hashtags.json",
params: {
slugs:
invalid_slugs + child_slugs + deeply_nested_slugs +
%w[foo foo:bar bar:baz qux qux:bar],
order: %w[category tag],
}
expect(response.status).to eq(200)
found_categories = response.parsed_body["category"]
expect(found_categories.map { |c| c["ref"] }).to match_array(
%w[foo foo:bar bar:baz qux qux:bar],
)
expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url)
expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq(
foobar.url,
)
expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq(
foobarbaz.url,
)
expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url)
expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq(
quxbar.url,
)
end
end
end
context "when enable_experimental_hashtag_autocomplete enabled" do
before { SiteSetting.enable_experimental_hashtag_autocomplete = true }
context "when logged in" do
context "as regular user" do
before { sign_in(Fabricate(:user)) }
it "returns only valid categories and tags" do
get "/hashtags.json",
params: {
slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name],
order: %w[category tag],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [
{
"relative_url" => category.url,
"text" => category.name,
"description" => nil,
"icon" => "folder",
"type" => "category",
"ref" => category.slug,
"slug" => category.slug,
"id" => category.id,
},
],
"tag" => [
{
"relative_url" => tag.url,
"text" => tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => tag.name,
"slug" => tag.name,
"secondary_text" => "x0",
"id" => tag.id,
},
],
},
)
end
it "handles tags with the ::tag type suffix" do
get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] }
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [],
"tag" => [
{
"relative_url" => tag.url,
"text" => tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => "#{tag.name}::tag",
"slug" => tag.name,
"secondary_text" => "x0",
"id" => tag.id,
},
],
},
)
end
it "does not return restricted categories or hidden tags" do
get "/hashtags.json",
params: {
slugs: [private_category.slug, hidden_tag.name],
order: %w[category tag],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq({ "category" => [], "tag" => [] })
end
end
context "as admin" do
fab!(:admin) { Fabricate(:admin) }
before { sign_in(admin) }
it "returns restricted categories and hidden tags" do
group.add(admin)
get "/hashtags.json",
params: {
slugs: [private_category.slug, hidden_tag.name],
order: %w[category tag],
}
expect(response.status).to eq(200)
expect(response.parsed_body).to eq(
{
"category" => [
{
"relative_url" => private_category.url,
"text" => private_category.name,
"description" => nil,
"icon" => "folder",
"type" => "category",
"ref" => private_category.slug,
"slug" => private_category.slug,
"id" => private_category.id,
},
],
"tag" => [
{
"relative_url" => hidden_tag.url,
"text" => hidden_tag.name,
"description" => nil,
"icon" => "tag",
"type" => "tag",
"ref" => hidden_tag.name,
"slug" => hidden_tag.name,
"secondary_text" => "x0",
"id" => hidden_tag.id,
},
],
},
)
end
end
context "with sub-sub-categories" do
before do
SiteSetting.max_category_nesting = 3
sign_in(Fabricate(:user))
end
it "works" do
foo = Fabricate(:category_with_definition, slug: "foo")
foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id)
foobarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id)
qux = Fabricate(:category_with_definition, slug: "qux")
quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id)
quxbarbaz =
Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id)
invalid_slugs = [":"]
child_slugs = %w[bar baz]
deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz]
get "/hashtags.json",
params: {
slugs:
invalid_slugs + child_slugs + deeply_nested_slugs +
%w[foo foo:bar bar:baz qux qux:bar],
order: %w[category tag],
}
expect(response.status).to eq(200)
found_categories = response.parsed_body["category"]
expect(found_categories.map { |c| c["ref"] }).to match_array(
%w[foo foo:bar bar:baz qux qux:bar],
)
expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url)
expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq(
foobar.url,
)
expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq(
foobarbaz.url,
)
expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url)
expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq(
quxbar.url,
)
end
end
end
context "when not logged in" do
it "returns invalid access" do
get "/hashtags.json", params: { slugs: [], order: %w[category tag] }
expect(response.status).to eq(403)
end
context "when not logged in" do
it "returns invalid access" do
get "/hashtags.json", params: { slugs: [], order: %w[category tag] }
expect(response.status).to eq(403)
end
end
end

View File

@ -313,53 +313,6 @@ RSpec.describe HashtagAutocompleteService do
end
end
describe "#lookup_old" do
fab!(:tag2) { Fabricate(:tag, name: "fiction-books") }
it "returns categories and tags in a hash format with the slug and url" do
result = service.lookup_old(%w[the-book-club great-books fiction-books])
expect(result[:categories]).to eq({ "the-book-club" => "/c/the-book-club/#{category1.id}" })
expect(result[:tags]).to eq(
{
"fiction-books" => "http://test.localhost/tag/fiction-books",
"great-books" => "http://test.localhost/tag/great-books",
},
)
end
it "does not include categories the user cannot access" do
category1.update!(read_restricted: true)
result = service.lookup_old(%w[the-book-club great-books fiction-books])
expect(result[:categories]).to eq({})
end
it "does not include tags the user cannot access" do
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["great-books"])
result = service.lookup_old(%w[the-book-club great-books fiction-books])
expect(result[:tags]).to eq({ "fiction-books" => "http://test.localhost/tag/fiction-books" })
end
it "handles tags which have the ::tag suffix" do
result = service.lookup_old(%w[the-book-club great-books::tag fiction-books])
expect(result[:tags]).to eq(
{
"fiction-books" => "http://test.localhost/tag/fiction-books",
"great-books" => "http://test.localhost/tag/great-books",
},
)
end
context "when not tagging_enabled" do
before { SiteSetting.tagging_enabled = false }
it "does not return tags" do
result = service.lookup_old(%w[the-book-club great-books fiction-books])
expect(result[:categories]).to eq({ "the-book-club" => "/c/the-book-club/#{category1.id}" })
expect(result[:tags]).to eq({})
end
end
end
describe "#lookup" do
fab!(:tag2) { Fabricate(:tag, name: "fiction-books") }

View File

@ -1332,8 +1332,7 @@ RSpec.describe PostAlerter do
expect(JSON.parse(body)).to eq(payload)
end
it "does not have invalid HTML in the excerpt when enable_experimental_hashtag_autocomplete is enabled" do
SiteSetting.enable_experimental_hashtag_autocomplete = true
it "does not have invalid HTML in the excerpt" do
Fabricate(:category, slug: "random")
Jobs.run_immediately!
body = nil

View File

@ -2,7 +2,7 @@
describe "Using #hashtag autocompletion to search for and lookup categories and tags",
type: :system do
fab!(:user) { Fabricate(:user) }
fab!(:current_user) { Fabricate(:user) }
fab!(:category) do
Fabricate(:category, name: "Cool Category", slug: "cool-cat", topic_count: 3234)
end
@ -16,10 +16,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) }
let(:topic_page) { PageObjects::Pages::Topic.new }
before do
SiteSetting.enable_experimental_hashtag_autocomplete = true
sign_in user
end
before { sign_in(current_user) }
def visit_topic_and_initiate_autocomplete(initiation_text: "something #co", expected_count: 2)
topic_page.visit_topic_and_open_composer(topic)

View File

@ -9,9 +9,8 @@ RSpec.describe "tasks/hashtags" do
describe "hashtag:mark_old_format_for_rebake" do
fab!(:category) { Fabricate(:category, slug: "support") }
before { SiteSetting.enable_experimental_hashtag_autocomplete = false }
it "sets the baked_version to 0 for matching posts" do
hashtag_html = PrettyText.cook("#support").gsub("<p>", "").gsub("</p>", "")
post_1 = Fabricate(:post, raw: "This is a cool #support hashtag")
post_2 =
Fabricate(
@ -19,10 +18,12 @@ RSpec.describe "tasks/hashtags" do
raw:
"Some other thing which will not match <a class=\"hashtag-wow\">some weird custom thing</a>",
)
SiteSetting.enable_experimental_hashtag_autocomplete = true
post_3 = Fabricate(:post, raw: "This is a cool #support hashtag")
SiteSetting.enable_experimental_hashtag_autocomplete = false
# Update to use the old hashtag format.
post_1.update!(
cooked: post_1.cooked.gsub(hashtag_html, "<span class=\"hashtag\"'>#support</span>"),
)
capture_stdout { Rake::Task["hashtags:mark_old_format_for_rebake"].invoke }