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"}} + +
+
+ +
+ + {{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" + ); +});