diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
index 48eb3dc3949..bc8ac812b03 100644
--- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
@@ -94,6 +94,10 @@ export default Ember.Component.extend(PanEvents, {
if (this.get("info.topicProgressExpanded")) {
$(".timeline-fullscreen").removeClass("show");
Ember.run.later(() => {
+ if (!this.element || this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
this.set("info.topicProgressExpanded", false);
this._checkSize();
}, 500);
@@ -102,11 +106,13 @@ export default Ember.Component.extend(PanEvents, {
keyboardTrigger(e) {
if (e.type === "jump") {
- const controller = showModal("jump-to-post");
+ const controller = showModal("jump-to-post", {
+ modalClass: "jump-to-post-modal"
+ });
controller.setProperties({
topic: this.get("topic"),
- postNumber: 1,
- jumpToIndex: this.attrs.jumpToIndex
+ jumpToIndex: this.attrs.jumpToIndex,
+ jumpToDate: this.attrs.jumpToDate
});
}
},
diff --git a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
index c1a2f4a20d2..bddaa746c7c 100644
--- a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
+++ b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
@@ -3,21 +3,41 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, {
model: null,
postNumber: null,
+ postDate: null,
+ filteredPostsCount: Ember.computed.alias(
+ "topic.postStream.filteredPostsCount"
+ ),
- onShow: () => {
+ onShow() {
Ember.run.next(() => $("#post-jump").focus());
},
actions: {
jump() {
- const max = this.get("topic.postStream.filteredPostsCount");
- const where = Math.min(
- max,
- Math.max(1, parseInt(this.get("postNumber")))
- );
-
- this.jumpToIndex(where);
- this.send("closeModal");
+ if (this.get("postNumber")) {
+ this._jumpToIndex(
+ this.get("filteredPostsCount"),
+ this.get("postNumber")
+ );
+ } else if (this.get("postDate")) {
+ this._jumpToDate(this.get("postDate"));
+ }
}
+ },
+
+ _jumpToIndex(postsCounts, postNumber) {
+ const where = Math.min(postsCounts, Math.max(1, parseInt(postNumber)));
+ this.jumpToIndex(where);
+ this._close();
+ },
+
+ _jumpToDate(date) {
+ this.jumpToDate(date);
+ this._close();
+ },
+
+ _close() {
+ this.setProperties({ postNumber: null, postDate: null });
+ this.send("closeModal");
}
});
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 4054182802d..fab0f360540 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -575,15 +575,20 @@ export default Ember.Controller.extend(BufferedContent, {
this._jumpToIndex(index);
},
+ jumpToDate(date) {
+ this._jumpToDate(date);
+ },
+
jumpToPostPrompt() {
const topic = this.get("model");
- const controller = showModal("jump-to-post");
+ const controller = showModal("jump-to-post", {
+ modalClass: "jump-to-post-modal"
+ });
controller.setProperties({
- topic: topic,
+ topic,
postNumber: null,
- jumpToIndex: index => {
- this.send("jumpToIndex", index);
- }
+ jumpToIndex: index => this.send("jumpToIndex", index),
+ jumpToDate: date => this.send("jumpToDate", date)
});
},
@@ -940,6 +945,21 @@ export default Ember.Controller.extend(BufferedContent, {
}
},
+ _jumpToDate(date) {
+ const postStream = this.get("model.postStream");
+
+ postStream
+ .loadNearestPostToDate(date)
+ .then(post => {
+ DiscourseURL.routeTo(
+ this.get("model").urlForPostNumber(post.get("post_number"))
+ );
+ })
+ .catch(() => {
+ this._jumpToIndex(postStream.get("topic.highest_post_number"));
+ });
+ },
+
_jumpToPostNumber(postNumber) {
const postStream = this.get("model.postStream");
const post = postStream.get("posts").findBy("post_number", postNumber);
diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6
index 04f55c7f319..9bed8773b09 100644
--- a/app/assets/javascripts/discourse/models/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/models/post-stream.js.es6
@@ -565,6 +565,15 @@ export default RestModel.extend({
});
},
+ loadNearestPostToDate(date) {
+ const url = `/posts/by-date/${this.get("topic.id")}/${date}`;
+ const store = this.store;
+
+ return ajax(url).then(post => {
+ return this.storePost(store.createRecord("post", post));
+ });
+ },
+
loadPost(postId) {
const url = "/posts/" + postId;
const store = this.store;
diff --git a/app/assets/javascripts/discourse/templates/modal/jump-to-post.hbs b/app/assets/javascripts/discourse/templates/modal/jump-to-post.hbs
index 187f47d9932..acce5e47d6e 100644
--- a/app/assets/javascripts/discourse/templates/modal/jump-to-post.hbs
+++ b/app/assets/javascripts/discourse/templates/modal/jump-to-post.hbs
@@ -1,8 +1,29 @@
{{#d-modal-body title="topic.progress.jump_prompt_long"}}
- {{input id="post-jump" type="number" value=postNumber insert-newline="jump"}}
-
- {{i18n "topic.progress.jump_prompt_of" count=topic.postStream.filteredPostsCount}}
-
+
+
+
+ #
+ {{input id="post-jump" type="number" value=postNumber insert-newline="jump"}}
+
+ {{i18n "topic.progress.jump_prompt_of" count=filteredPostsCount}}
+
+
+
+
+
+
+ {{i18n "topic.progress.jump_prompt_or"}}
+
+
+
+
+
+
+ {{i18n "topic.progress.jump_prompt_to_date"}}
+
+ {{date-picker id="post-date" class="date-input" value=postDate defaultDate="YYYY-MM-DD"}}
+
+
{{/d-modal-body}}
- {{#topic-navigation topic=model jumpToIndex=(action "jumpToIndex") as |info|}}
+ {{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
{{#if info.renderTimeline}}
{{#if info.renderAdminMenuButton}}
{{topic-admin-menu-button
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index 465a64c32fc..48ab4dfa733 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -525,6 +525,40 @@
}
}
+.jump-to-post-modal {
+ .modal-body {
+ #post-jump,
+ .date-picker {
+ margin: 0;
+ width: 100px;
+ }
+
+ .pika-single {
+ position: relative !important;
+ }
+
+ .jump-to-post-control .index {
+ color: $primary-medium;
+ }
+
+ .separator {
+ display: flex;
+ align-items: center;
+ margin: 1em auto;
+
+ .left,
+ .right {
+ flex: 1;
+ }
+
+ .text {
+ margin: 0 0.5em;
+ color: $primary-medium;
+ }
+ }
+ }
+}
+
.tabbed-modal {
.modal-body {
position: relative;
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 6cb8aa6f4b1..2e92194cfe8 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -12,6 +12,7 @@ class PostsController < ApplicationController
:show,
:replies,
:by_number,
+ :by_date,
:short_link,
:reply_history,
:replyIids,
@@ -249,6 +250,11 @@ class PostsController < ApplicationController
display_post(post)
end
+ def by_date
+ post = find_post_from_params_by_date
+ display_post(post)
+ end
+
def reply_history
post = find_post_from_params
render_serialized(post.reply_history(params[:max_replies].to_i, guardian), PostSerializer)
@@ -707,6 +713,16 @@ class PostsController < ApplicationController
find_post_using(by_number_finder)
end
+ def find_post_from_params_by_date
+ by_date_finder = TopicView.new(params[:topic_id], current_user)
+ .filtered_posts
+ .where("created_at >= ?", Time.zone.parse(params[:date]))
+ .order("created_at ASC")
+ .limit(1)
+
+ find_post_using(by_date_finder)
+ end
+
def find_post_using(finder)
# Include deleted posts if the user is staff
finder = finder.with_deleted if current_user.try(:staff?)
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 2ec1caf0ea9..ff1e33ac11c 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1777,6 +1777,8 @@ en:
jump_prompt_of: "of %{count} posts"
jump_prompt_long: "What post would you like to jump to?"
jump_bottom_with_number: "jump to post %{post_number}"
+ jump_prompt_to_date: "to date"
+ jump_prompt_or: "or"
total: total posts
current: current post
diff --git a/config/routes.rb b/config/routes.rb
index 79536cbf4f8..f969cdd3e1d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -457,6 +457,7 @@ Discourse::Application.routes.draw do
get "posts" => "posts#latest", id: "latest_posts"
get "private-posts" => "posts#latest", id: "private_posts"
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
+ get "posts/by-date/:topic_id/:date" => "posts#by_date"
get "posts/:id/reply-history" => "posts#reply_history"
get "posts/:id/reply-ids" => "posts#reply_ids"
get "posts/:id/reply-ids/all" => "posts#all_reply_ids"
diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb
index 22dd8c25829..201b94bcfde 100644
--- a/spec/requests/posts_controller_spec.rb
+++ b/spec/requests/posts_controller_spec.rb
@@ -92,6 +92,30 @@ describe PostsController do
end
end
+ describe '#by_date' do
+ include_examples 'finding and showing post' do
+ let(:url) { "/posts/by-date/#{post.topic_id}/#{post.created_at.strftime("%Y-%m-%d")}.json" }
+ end
+
+ it 'returns the expected post' do
+ first_post = Fabricate(:post, created_at: 10.days.ago)
+ second_post = Fabricate(:post, topic: first_post.topic, created_at: 4.days.ago)
+ third_post = Fabricate(:post, topic: first_post.topic, created_at: 3.days.ago)
+
+ get "/posts/by-date/#{second_post.topic_id}/#{(second_post.created_at - 2.days).strftime("%Y-%m-%d")}.json"
+ json = JSON.parse(response.body)
+
+ expect(response.status).to eq(200)
+ expect(json["id"]).to eq(second_post.id)
+ end
+
+ it 'returns no post if date is > at last created post' do
+ get "/posts/by-date/#{post.topic_id}/2245-11-11.json"
+ json = JSON.parse(response.body)
+ expect(response.status).to eq(404)
+ end
+ end
+
describe '#reply_history' do
include_examples 'finding and showing post' do
let(:url) { "/posts/#{post.id}/reply-history.json" }
diff --git a/test/javascripts/acceptance/jump-to-test.js.es6 b/test/javascripts/acceptance/jump-to-test.js.es6
new file mode 100644
index 00000000000..87856db860b
--- /dev/null
+++ b/test/javascripts/acceptance/jump-to-test.js.es6
@@ -0,0 +1,51 @@
+import { acceptance } from "helpers/qunit-helpers";
+
+acceptance("Jump to", {
+ loggedIn: true,
+
+ pretend(server, helper) {
+ server.get("/t/280/excerpts.json", () => helper.response(200, []));
+ server.get("/t/280/3.json", () => helper.response(200, {}));
+ server.get("/posts/by-date/280/:date", req => {
+ if (req.params["date"] === "2014-02-24") {
+ return helper.response(200, {
+ post_number: 3
+ });
+ }
+
+ return helper.response(404, null);
+ });
+ }
+});
+
+QUnit.test("default", async assert => {
+ await visit("/t/internationalization-localization/280");
+
+ await click("nav#topic-progress .nums");
+ await click("button.jump-to-post");
+
+ assert.ok(exists(".jump-to-post-modal"), "it shows the modal");
+
+ await fillIn("input.date-picker", "2014-02-24");
+ await click(".jump-to-post-modal .btn-primary");
+
+ assert.equal(
+ currentURL(),
+ "/t/internationalization-localization/280/3",
+ "it jumps to the correct post"
+ );
+});
+
+QUnit.test("invalid date", async assert => {
+ await visit("/t/internationalization-localization/280");
+ await click("nav#topic-progress .nums");
+ await click("button.jump-to-post");
+ await fillIn("input.date-picker", "2094-02-24");
+ await click(".jump-to-post-modal .btn-primary");
+
+ assert.equal(
+ currentURL(),
+ "/t/internationalization-localization/280/20",
+ "it jumps to the last post if no post found"
+ );
+});