FEATURE: allows to jump to a date in a topic

This commit is contained in:
Joffrey JAFFEUX 2018-07-19 16:00:13 +02:00 committed by GitHub
parent 7f7944bc3a
commit a2281fbb19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 226 additions and 22 deletions

View File

@ -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
});
}
},

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -1,8 +1,29 @@
{{#d-modal-body title="topic.progress.jump_prompt_long"}}
{{input id="post-jump" type="number" value=postNumber insert-newline="jump"}}
<span class='input-hint-text'>
{{i18n "topic.progress.jump_prompt_of" count=topic.postStream.filteredPostsCount}}
</span>
<div class="jumpt-to-post-form">
<div class="jump-to-post-control">
<span class="index">#</span>
{{input id="post-jump" type="number" value=postNumber insert-newline="jump"}}
<span class='input-hint-text post-number'>
{{i18n "topic.progress.jump_prompt_of" count=filteredPostsCount}}
</span>
</div>
<div class="separator">
<hr class="left" />
<span class="text">
{{i18n "topic.progress.jump_prompt_or"}}
</span>
<hr class="right" />
</div>
<div class="jump-to-date-control">
<span class='input-hint-text post-date'>
{{i18n "topic.progress.jump_prompt_to_date"}}
</span>
{{date-picker id="post-date" class="date-input" value=postDate defaultDate="YYYY-MM-DD"}}
</div>
</div>
{{/d-modal-body}}
<div class='modal-footer'>

View File

@ -78,7 +78,7 @@
{{partial "selected-posts"}}
</div>
{{#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

View File

@ -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;

View File

@ -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?)

View File

@ -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

View File

@ -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"

View File

@ -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" }

View File

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