mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 03:33:39 +08:00
Support for "Select All / Deselect All" while selecting posts to merge / delete.
This commit is contained in:
parent
7daca77443
commit
a80ec535a3
|
@ -15,41 +15,48 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
loadingBelow: false,
|
loadingBelow: false,
|
||||||
loadingAbove: false,
|
loadingAbove: false,
|
||||||
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
||||||
|
allPostsSelected: false,
|
||||||
|
selectedPosts: new Em.Set(),
|
||||||
|
|
||||||
selectedPosts: function() {
|
canMergeTopic: function() {
|
||||||
var posts = this.get('content.posts');
|
if (!this.get('can_move_posts')) return false;
|
||||||
if (!posts) return null;
|
|
||||||
return posts.filterProperty('selected');
|
|
||||||
}.property('content.posts.@each.selected'),
|
|
||||||
|
|
||||||
canMoveSelected: function() {
|
|
||||||
if (!this.get('content.can_move_posts')) return false;
|
|
||||||
return (this.get('selectedPostsCount') > 0);
|
return (this.get('selectedPostsCount') > 0);
|
||||||
}.property('canDeleteSelected'),
|
}.property('selectedPostsCount'),
|
||||||
|
|
||||||
|
canSplitTopic: function() {
|
||||||
|
if (!this.get('can_move_posts')) return false;
|
||||||
|
if (this.get('allPostsSelected')) return false;
|
||||||
|
return (this.get('selectedPostsCount') > 0);
|
||||||
|
}.property('selectedPostsCount'),
|
||||||
|
|
||||||
|
|
||||||
|
canSelectAll: Em.computed.not('allPostsSelected'),
|
||||||
|
|
||||||
|
canDeselectAll: function () {
|
||||||
|
if (this.get('selectedPostsCount') > 0) return true;
|
||||||
|
if (this.get('allPostsSelected')) return true;
|
||||||
|
}.property('selectedPostsCount', 'allPostsSelected'),
|
||||||
|
|
||||||
canDeleteSelected: function() {
|
canDeleteSelected: function() {
|
||||||
var selectedPosts = this.get('selectedPosts');
|
var selectedPosts = this.get('selectedPosts');
|
||||||
|
|
||||||
|
if (this.get('allPostsSelected')) return true;
|
||||||
if (this.get('selectedPostsCount') === 0) return false;
|
if (this.get('selectedPostsCount') === 0) return false;
|
||||||
|
|
||||||
var canDelete = true;
|
var canDelete = true;
|
||||||
selectedPosts.each(function(p) {
|
selectedPosts.forEach(function(p) {
|
||||||
if (!p.get('can_delete')) {
|
if (!p.get('can_delete')) {
|
||||||
canDelete = false;
|
canDelete = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return canDelete;
|
return canDelete;
|
||||||
}.property('selectedPosts'),
|
}.property('selectedPostsCount'),
|
||||||
|
|
||||||
multiSelectChanged: function() {
|
multiSelectChanged: function() {
|
||||||
// Deselect all posts when multi select is turned off
|
// Deselect all posts when multi select is turned off
|
||||||
if (!this.get('multiSelect')) {
|
if (!this.get('multiSelect')) {
|
||||||
var posts = this.get('content.posts');
|
this.deselectAll();
|
||||||
if (posts) {
|
|
||||||
posts.forEach(function(p) {
|
|
||||||
p.set('selected', false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.observes('multiSelect'),
|
}.observes('multiSelect'),
|
||||||
|
|
||||||
|
@ -61,7 +68,32 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
}.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
|
}.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
|
||||||
|
|
||||||
selectPost: function(post) {
|
selectPost: function(post) {
|
||||||
post.toggleProperty('selected');
|
var selectedPosts = this.get('selectedPosts');
|
||||||
|
if (selectedPosts.contains(post)) {
|
||||||
|
selectedPosts.removeObject(post);
|
||||||
|
this.set('allPostsSelected', false);
|
||||||
|
} else {
|
||||||
|
selectedPosts.addObject(post);
|
||||||
|
|
||||||
|
// If the user manually selects all posts, all posts are selected
|
||||||
|
if (selectedPosts.length === this.get('posts_count')) {
|
||||||
|
this.set('allPostsSelected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAll: function() {
|
||||||
|
var posts = this.get('posts');
|
||||||
|
var selectedPosts = this.get('selectedPosts');
|
||||||
|
if (posts) {
|
||||||
|
selectedPosts.addObjects(posts);
|
||||||
|
}
|
||||||
|
this.set('allPostsSelected', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
deselectAll: function() {
|
||||||
|
this.get('selectedPosts').clear();
|
||||||
|
this.set('allPostsSelected', false);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleMultiSelect: function() {
|
toggleMultiSelect: function() {
|
||||||
|
@ -90,14 +122,21 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
modalController.show(Discourse.MergeTopicView.create({
|
modalController.show(Discourse.MergeTopicView.create({
|
||||||
topicController: this,
|
topicController: this,
|
||||||
topic: this.get('content'),
|
topic: this.get('content'),
|
||||||
|
allPostsSelected: this.get('allPostsSelected'),
|
||||||
selectedPosts: this.get('selectedPosts')
|
selectedPosts: this.get('selectedPosts')
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteSelected: function() {
|
deleteSelected: function() {
|
||||||
var topicController = this;
|
var topicController = this;
|
||||||
return bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
||||||
|
// If all posts are selected, it's the same thing as deleting the topic
|
||||||
|
if (topicController.get('allPostsSelected')) {
|
||||||
|
return topicController.deleteTopic();
|
||||||
|
}
|
||||||
|
|
||||||
var selectedPosts = topicController.get('selectedPosts');
|
var selectedPosts = topicController.get('selectedPosts');
|
||||||
Discourse.Post.deleteMany(selectedPosts);
|
Discourse.Post.deleteMany(selectedPosts);
|
||||||
topicController.get('content.posts').removeObjects(selectedPosts);
|
topicController.get('content.posts').removeObjects(selectedPosts);
|
||||||
|
@ -245,7 +284,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
});
|
});
|
||||||
}.observes('postFilters'),
|
}.observes('postFilters'),
|
||||||
|
|
||||||
deleteTopic: function(e) {
|
deleteTopic: function() {
|
||||||
var topicController = this;
|
var topicController = this;
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
this.get('content').destroy().then(function() {
|
this.get('content').destroy().then(function() {
|
||||||
|
|
|
@ -10,8 +10,11 @@ Discourse.SelectedPostsCount = Em.Mixin.create({
|
||||||
|
|
||||||
selectedPostsCount: function() {
|
selectedPostsCount: function() {
|
||||||
if (!this.get('selectedPosts')) return 0;
|
if (!this.get('selectedPosts')) return 0;
|
||||||
return this.get('selectedPosts').length;
|
|
||||||
}.property('selectedPosts')
|
if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count');
|
||||||
|
|
||||||
|
return this.get('selectedPosts.length');
|
||||||
|
}.property('selectedPosts.length', 'allPostsSelected')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -449,6 +449,17 @@ Discourse.Topic.reopenClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mergeTopic: function(topicId, destinationTopicId) {
|
||||||
|
var promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
|
||||||
|
type: 'POST',
|
||||||
|
data: {destination_topic_id: destinationTopicId}
|
||||||
|
}).then(function (result) {
|
||||||
|
if (result.success) return result;
|
||||||
|
promise.reject();
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
|
||||||
movePosts: function(topicId, opts) {
|
movePosts: function(topicId, opts) {
|
||||||
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
|
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
<p>{{countI18n topic.multi_select.description countBinding="controller.selectedPostsCount"}}</p>
|
<p>{{countI18n topic.multi_select.description countBinding="controller.selectedPostsCount"}}</p>
|
||||||
|
|
||||||
|
{{#if canSelectAll}}
|
||||||
|
<p><a href='#' {{action selectAll}}>select all</a></p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canDeselectAll}}
|
||||||
|
<p><a href='#' {{action deselectAll}}>deselect all</a></p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if canDeleteSelected}}
|
{{#if canDeleteSelected}}
|
||||||
<button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button>
|
<button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canMoveSelected}}
|
{{#if canSplitTopic}}
|
||||||
<button class='btn' {{action splitTopic}}><i class='icon icon-move'></i> {{i18n topic.split_topic.action}}</button>
|
<button class='btn' {{action splitTopic}}><i class='icon icon-move'></i> {{i18n topic.split_topic.action}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{#if canMergeTopic}}
|
||||||
<button class='btn' {{action mergeTopic}}><i class='icon icon-move'></i> {{i18n topic.merge_topic.action}}}</button>
|
<button class='btn' {{action mergeTopic}}><i class='icon icon-move'></i> {{i18n topic.merge_topic.action}}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<button {{action togglePinned}} class='btn btn-admin'><i class='icon-pushpin'></i> {{i18n topic.actions.pin}}</button>
|
<button {{action togglePinned}} class='btn btn-admin'><i class='icon-pushpin'></i> {{i18n topic.actions.pin}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
{{#if content.archived}}
|
{{#if content.archived}}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<button {{action toggleArchived}} class='btn btn-admin'><i class='icon-folder-close'></i> {{i18n topic.actions.archive}}</button>
|
<button {{action toggleArchived}} class='btn btn-admin'><i class='icon-folder-close'></i> {{i18n topic.actions.archive}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
{{#if content.visible}}
|
{{#if content.visible}}
|
||||||
|
|
|
@ -23,13 +23,20 @@ Discourse.MergeTopicView = Discourse.ModalBodyView.extend(Discourse.SelectedPost
|
||||||
movePostsToExistingTopic: function() {
|
movePostsToExistingTopic: function() {
|
||||||
this.set('saving', true);
|
this.set('saving', true);
|
||||||
|
|
||||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
|
||||||
var moveSelectedView = this;
|
var moveSelectedView = this;
|
||||||
|
|
||||||
Discourse.Topic.movePosts(this.get('topic.id'), {
|
var promise = null;
|
||||||
destination_topic_id: this.get('selectedTopicId'),
|
if (this.get('allPostsSelected')) {
|
||||||
post_ids: postIds
|
promise = Discourse.Topic.mergeTopic(this.get('topic.id'), this.get('selectedTopicId'));
|
||||||
}).then(function(result) {
|
} else {
|
||||||
|
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||||
|
promise = Discourse.Topic.movePosts(this.get('topic.id'), {
|
||||||
|
destination_topic_id: this.get('selectedTopicId'),
|
||||||
|
post_ids: postIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(function(result) {
|
||||||
// Posts moved
|
// Posts moved
|
||||||
$('#discourse-modal').modal('hide');
|
$('#discourse-modal').modal('hide');
|
||||||
moveSelectedView.get('topicController').toggleMultiSelect();
|
moveSelectedView.get('topicController').toggleMultiSelect();
|
||||||
|
|
|
@ -11,14 +11,14 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
templateName: 'post',
|
templateName: 'post',
|
||||||
classNameBindings: ['post.lastPost',
|
classNameBindings: ['post.lastPost',
|
||||||
'postTypeClass',
|
'postTypeClass',
|
||||||
'post.selected',
|
'selected',
|
||||||
'post.hidden:hidden',
|
'post.hidden:hidden',
|
||||||
'post.deleted_at:deleted',
|
'post.deleted_at:deleted',
|
||||||
'parentPost:replies-above'],
|
'parentPost:replies-above'],
|
||||||
postBinding: 'content',
|
postBinding: 'content',
|
||||||
|
|
||||||
// TODO really we should do something cleaner here... this makes it work in debug but feels really messy
|
// TODO really we should do something cleaner here... this makes it work in debug but feels really messy
|
||||||
screenTrack: (function() {
|
screenTrack: function() {
|
||||||
var parentView = this.get('parentView');
|
var parentView = this.get('parentView');
|
||||||
var screenTrack = null;
|
var screenTrack = null;
|
||||||
while (parentView && !screenTrack) {
|
while (parentView && !screenTrack) {
|
||||||
|
@ -26,17 +26,17 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
parentView = parentView.get('parentView');
|
parentView = parentView.get('parentView');
|
||||||
}
|
}
|
||||||
return screenTrack;
|
return screenTrack;
|
||||||
}).property('parentView'),
|
}.property('parentView'),
|
||||||
|
|
||||||
postTypeClass: (function() {
|
postTypeClass: function() {
|
||||||
return this.get('post.post_type') === Discourse.get('site.post_types.moderator_action') ? 'moderator' : 'regular';
|
return this.get('post.post_type') === Discourse.get('site.post_types.moderator_action') ? 'moderator' : 'regular';
|
||||||
}).property('post.post_type'),
|
}.property('post.post_type'),
|
||||||
|
|
||||||
// If the cooked content changed, add the quote controls
|
// If the cooked content changed, add the quote controls
|
||||||
cookedChanged: (function() {
|
cookedChanged: function() {
|
||||||
var postView = this;
|
var postView = this;
|
||||||
Em.run.next(function() { postView.insertQuoteControls(); });
|
Em.run.next(function() { postView.insertQuoteControls(); });
|
||||||
}).observes('post.cooked'),
|
}.observes('post.cooked'),
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this._super();
|
this._super();
|
||||||
|
@ -45,13 +45,19 @@ Discourse.PostView = Discourse.View.extend({
|
||||||
|
|
||||||
mouseUp: function(e) {
|
mouseUp: function(e) {
|
||||||
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
|
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
|
||||||
this.toggleProperty('post.selected');
|
this.get('controller').selectPost(this.get('post'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
selected: function() {
|
||||||
|
var selectedPosts = this.get('controller.selectedPosts');
|
||||||
|
if (!selectedPosts) return false;
|
||||||
|
return selectedPosts.contains(this.get('post'));
|
||||||
|
}.property('controller.selectedPostsCount'),
|
||||||
|
|
||||||
selectText: function() {
|
selectText: function() {
|
||||||
return this.get('post.selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : Em.String.i18n('topic.multi_select.select');
|
return this.get('selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : Em.String.i18n('topic.multi_select.select');
|
||||||
}.property('post.selected', 'controller.selectedPostsCount'),
|
}.property('selected', 'controller.selectedPostsCount'),
|
||||||
|
|
||||||
repliesHidden: function() {
|
repliesHidden: function() {
|
||||||
return !this.get('repliesShown');
|
return !this.get('repliesShown');
|
||||||
|
|
|
@ -15,6 +15,7 @@ class TopicsController < ApplicationController
|
||||||
:unmute,
|
:unmute,
|
||||||
:set_notifications,
|
:set_notifications,
|
||||||
:move_posts,
|
:move_posts,
|
||||||
|
:merge_topic,
|
||||||
:clear_pin,
|
:clear_pin,
|
||||||
:autoclose]
|
:autoclose]
|
||||||
|
|
||||||
|
@ -137,6 +138,20 @@ class TopicsController < ApplicationController
|
||||||
render json: success_json
|
render json: success_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_topic
|
||||||
|
requires_parameters(:destination_topic_id)
|
||||||
|
|
||||||
|
topic = Topic.where(id: params[:topic_id]).first
|
||||||
|
guardian.ensure_can_move_posts!(topic)
|
||||||
|
|
||||||
|
dest_topic = topic.move_posts(current_user, topic.posts.pluck(:id), destination_topic_id: params[:destination_topic_id].to_i)
|
||||||
|
if dest_topic.present?
|
||||||
|
render json: {success: true, url: dest_topic.relative_url}
|
||||||
|
else
|
||||||
|
render json: {success: false}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def move_posts
|
def move_posts
|
||||||
requires_parameters(:post_ids)
|
requires_parameters(:post_ids)
|
||||||
|
|
||||||
|
@ -153,7 +168,6 @@ class TopicsController < ApplicationController
|
||||||
else
|
else
|
||||||
render json: {success: false}
|
render json: {success: false}
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_pin
|
def clear_pin
|
||||||
|
|
|
@ -212,6 +212,7 @@ Discourse::Application.routes.draw do
|
||||||
post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/}
|
||||||
post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/}
|
||||||
post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/}
|
post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/}
|
||||||
|
post 't/:topic_id/merge-topic' => 'topics#merge_topic', constraints: {topic_id: /\d+/}
|
||||||
delete 't/:topic_id/timings' => 'topics#destroy_timings', constraints: {topic_id: /\d+/}
|
delete 't/:topic_id/timings' => 'topics#destroy_timings', constraints: {topic_id: /\d+/}
|
||||||
|
|
||||||
post 't/:topic_id/notifications' => 'topics#set_notifications' , constraints: {topic_id: /\d+/}
|
post 't/:topic_id/notifications' => 'topics#set_notifications' , constraints: {topic_id: /\d+/}
|
||||||
|
|
|
@ -2,6 +2,8 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe TopicsController do
|
describe TopicsController do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
context 'move_posts' do
|
context 'move_posts' do
|
||||||
it 'needs you to be logged in' do
|
it 'needs you to be logged in' do
|
||||||
lambda { xhr :post, :move_posts, topic_id: 111, title: 'blah', post_ids: [1,2,3] }.should raise_error(Discourse::NotLoggedIn)
|
lambda { xhr :post, :move_posts, topic_id: 111, title: 'blah', post_ids: [1,2,3] }.should raise_error(Discourse::NotLoggedIn)
|
||||||
|
@ -95,6 +97,49 @@ describe TopicsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "merge_topic" do
|
||||||
|
it 'needs you to be logged in' do
|
||||||
|
lambda { xhr :post, :merge_topic, topic_id: 111, destination_topic_id: 345 }.should raise_error(Discourse::NotLoggedIn)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'moving to a new topic' do
|
||||||
|
let!(:user) { log_in(:moderator) }
|
||||||
|
let(:p1) { Fabricate(:post, user: user) }
|
||||||
|
let(:topic) { p1.topic }
|
||||||
|
|
||||||
|
it "raises an error without destination_topic_id" do
|
||||||
|
lambda { xhr :post, :merge_topic, topic_id: topic.id }.should raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error when the user doesn't have permission to merge" do
|
||||||
|
Guardian.any_instance.expects(:can_move_posts?).returns(false)
|
||||||
|
xhr :post, :merge_topic, topic_id: 111, destination_topic_id: 345
|
||||||
|
response.should be_forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:dest_topic) { Fabricate(:topic) }
|
||||||
|
|
||||||
|
context 'moves all the posts to the destination topic' do
|
||||||
|
let(:p2) { Fabricate(:post, user: user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Topic.any_instance.expects(:move_posts).with(user, [p1.id], destination_topic_id: dest_topic.id).returns(topic)
|
||||||
|
xhr :post, :merge_topic, topic_id: topic.id, destination_topic_id: dest_topic.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns success" do
|
||||||
|
response.should be_success
|
||||||
|
result = ::JSON.parse(response.body)
|
||||||
|
result['success'].should be_true
|
||||||
|
result['url'].should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
context 'similar_to' do
|
context 'similar_to' do
|
||||||
|
|
||||||
let(:title) { 'this title is long enough to search for' }
|
let(:title) { 'this title is long enough to search for' }
|
||||||
|
|
Loading…
Reference in New Issue
Block a user