FEATURE: Optionally delete bookmark when reminder sent (#9637)

We now show an options gear icon next to the bookmark name.

When expanded we show the "delete bookmark when reminder sent" option. The value of this checkbox is saved in local storage for the user.

If this is ticked, when a reminder is sent for the bookmark the bookmark itself is deleted. This is so people can use the reminder functionality by itself.

Also remove the blue alert reminder section from the "Edit Bookmark" modal as it just added clutter, because the user can already see they had a reminder set:

Adds a default false boolean column `delete_when_reminder_sent` to bookmarks.
This commit is contained in:
Martin Brennan
2020-05-07 13:37:39 +10:00
committed by GitHub
parent 423802fbce
commit 6fb0f36ce1
14 changed files with 196 additions and 31 deletions

View File

@ -1,4 +1,5 @@
import { and } from "@ember/object/computed"; import { and } from "@ember/object/computed";
import { next } from "@ember/runloop";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { isPresent } from "@ember/utils"; import { isPresent } from "@ember/utils";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
@ -59,6 +60,8 @@ export default Controller.extend(ModalFunctionality, {
lastCustomReminderTime: null, lastCustomReminderTime: null,
mouseTrap: null, mouseTrap: null,
userTimezone: null, userTimezone: null,
showOptions: false,
options: null,
onShow() { onShow() {
this.setProperties({ this.setProperties({
@ -70,9 +73,13 @@ export default Controller.extend(ModalFunctionality, {
customReminderTime: this._defaultCustomReminderTime(), customReminderTime: this._defaultCustomReminderTime(),
lastCustomReminderDate: null, lastCustomReminderDate: null,
lastCustomReminderTime: null, lastCustomReminderTime: null,
userTimezone: this.currentUser.resolvedTimezone() userTimezone: this.currentUser.resolvedTimezone(),
showOptions: false,
options: {},
model: this.model || {}
}); });
this._loadBookmarkOptions();
this._bindKeyboardShortcuts(); this._bindKeyboardShortcuts();
this._loadLastUsedCustomReminderDatetime(); this._loadLastUsedCustomReminderDatetime();
@ -124,6 +131,21 @@ export default Controller.extend(ModalFunctionality, {
return isPresent(this.model) && isPresent(this.model.reminderAt); return isPresent(this.model) && isPresent(this.model.reminderAt);
}, },
_loadBookmarkOptions() {
this.set(
"options.deleteWhenReminderSent",
this.model.deleteWhenReminderSent ||
localStorage.bookmarkOptionsDeleteWhenReminderSent === "true"
);
// we want to make sure the options panel opens so the user
// knows they have set these options previously. run next otherwise
// the modal is not visible when it tries to slide down the options
if (this.options.deleteWhenReminderSent) {
next(() => this.toggleOptionsPanel());
}
},
_loadLastUsedCustomReminderDatetime() { _loadLastUsedCustomReminderDatetime() {
let lastTime = localStorage.lastCustomBookmarkReminderTime; let lastTime = localStorage.lastCustomBookmarkReminderTime;
let lastDate = localStorage.lastCustomBookmarkReminderDate; let lastDate = localStorage.lastCustomBookmarkReminderDate;
@ -268,6 +290,8 @@ export default Controller.extend(ModalFunctionality, {
localStorage.lastCustomBookmarkReminderDate = this.customReminderDate; localStorage.lastCustomBookmarkReminderDate = this.customReminderDate;
} }
localStorage.bookmarkOptionsDeleteWhenReminderSent = this.options.deleteWhenReminderSent;
let reminderType; let reminderType;
if (this.selectedReminderType === REMINDER_TYPES.NONE) { if (this.selectedReminderType === REMINDER_TYPES.NONE) {
reminderType = null; reminderType = null;
@ -282,7 +306,8 @@ export default Controller.extend(ModalFunctionality, {
reminder_at: reminderAtISO, reminder_at: reminderAtISO,
name: this.model.name, name: this.model.name,
post_id: this.model.postId, post_id: this.model.postId,
id: this.model.id id: this.model.id,
delete_when_reminder_sent: this.options.deleteWhenReminderSent
}; };
if (this._editingExistingBookmark()) { if (this._editingExistingBookmark()) {
@ -294,6 +319,7 @@ export default Controller.extend(ModalFunctionality, {
this.afterSave({ this.afterSave({
reminderAt: reminderAtISO, reminderAt: reminderAtISO,
reminderType: this.selectedReminderType, reminderType: this.selectedReminderType,
deleteWhenReminderSent: this.options.deleteWhenReminderSent,
id: this.model.id, id: this.model.id,
name: this.model.name name: this.model.name
}); });
@ -305,6 +331,7 @@ export default Controller.extend(ModalFunctionality, {
this.afterSave({ this.afterSave({
reminderAt: reminderAtISO, reminderAt: reminderAtISO,
reminderType: this.selectedReminderType, reminderType: this.selectedReminderType,
deleteWhenReminderSent: this.options.deleteWhenReminderSent,
id: response.id, id: response.id,
name: this.model.name name: this.model.name
}); });
@ -420,6 +447,16 @@ export default Controller.extend(ModalFunctionality, {
} }
}, },
@action
toggleOptionsPanel() {
if (this.showOptions) {
$(".bookmark-options-panel").slideUp("fast");
} else {
$(".bookmark-options-panel").slideDown("fast");
}
this.toggleProperty("showOptions");
},
@action @action
saveAndClose() { saveAndClose() {
if (this._saving || this._deleting) { if (this._saving || this._deleting) {

View File

@ -316,6 +316,7 @@ const Post = RestModel.extend({
postId: this.id, postId: this.id,
id: this.bookmark_id, id: this.bookmark_id,
reminderAt: this.bookmark_reminder_at, reminderAt: this.bookmark_reminder_at,
deleteWhenReminderSent: this.bookmark_delete_when_reminder_sent,
name: this.bookmark_name name: this.bookmark_name
}, },
title: this.bookmark_id title: this.bookmark_id
@ -334,6 +335,8 @@ const Post = RestModel.extend({
bookmarked: true, bookmarked: true,
bookmark_reminder_at: savedData.reminderAt, bookmark_reminder_at: savedData.reminderAt,
bookmark_reminder_type: savedData.reminderType, bookmark_reminder_type: savedData.reminderType,
bookmark_delete_when_reminder_sent:
savedData.deleteWhenReminderSent,
bookmark_name: savedData.name, bookmark_name: savedData.name,
bookmark_id: savedData.id bookmark_id: savedData.id
}); });

View File

@ -8,8 +8,16 @@
</div> </div>
{{/if}} {{/if}}
<div class="control-group"> <div class="control-group bookmark-name-wrap">
{{input id="bookmark-name" value=model.name name="name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}} {{input id="bookmark-name" value=model.name name="name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}}
{{d-button icon="gear" action=(action "toggleOptionsPanel") class="bookmark-options-button"}}
</div>
<div class="bookmark-options-panel">
<label for="delete_when_reminder_sent">
{{i18n "bookmarks.delete_when_reminder_sent"}}
{{input id="delete_when_reminder_sent" type="checkbox" checked=options.deleteWhenReminderSent}}
</label>
</div> </div>
{{#if showExistingReminderAt }} {{#if showExistingReminderAt }}

View File

@ -62,4 +62,24 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
} }
.bookmark-name-wrap {
display: inline-flex;
width: 100%;
align-items: end;
}
.bookmark-options-button {
margin-left: 0.5em;
background: transparent;
}
.bookmark-options-panel {
margin-bottom: 18px;
display: none;
input[type="checkbox"] {
margin-right: 0.85em;
}
}
} }

View File

@ -11,7 +11,10 @@ class BookmarksController < ApplicationController
post_id: params[:post_id], post_id: params[:post_id],
name: params[:name], name: params[:name],
reminder_type: params[:reminder_type], reminder_type: params[:reminder_type],
reminder_at: params[:reminder_at] reminder_at: params[:reminder_at],
options: {
delete_when_reminder_sent: params[:delete_when_reminder_sent]
}
) )
if bookmark_manager.errors.empty? if bookmark_manager.errors.empty?
@ -35,7 +38,10 @@ class BookmarksController < ApplicationController
bookmark_id: params[:id], bookmark_id: params[:id],
name: params[:name], name: params[:name],
reminder_type: params[:reminder_type], reminder_type: params[:reminder_type],
reminder_at: params[:reminder_at] reminder_at: params[:reminder_at],
options: {
delete_when_reminder_sent: params[:delete_when_reminder_sent]
}
) )
if bookmark_manager.errors.empty? if bookmark_manager.errors.empty?

View File

@ -90,6 +90,7 @@ end
# updated_at :datetime not null # updated_at :datetime not null
# reminder_last_sent_at :datetime # reminder_last_sent_at :datetime
# reminder_set_at :datetime # reminder_set_at :datetime
# delete_when_reminder_sent :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View File

@ -53,6 +53,7 @@ class PostSerializer < BasicPostSerializer
:bookmark_id, :bookmark_id,
:bookmark_reminder_type, :bookmark_reminder_type,
:bookmark_name, :bookmark_name,
:bookmark_delete_when_reminder_sent,
:raw, :raw,
:actions_summary, :actions_summary,
:moderator?, :moderator?,
@ -331,6 +332,10 @@ class PostSerializer < BasicPostSerializer
include_bookmarked? include_bookmarked?
end end
def include_bookmark_delete_when_reminder_sent?
include_bookmarked?
end
def include_bookmark_id? def include_bookmark_id?
include_bookmarked? include_bookmarked?
end end
@ -353,6 +358,10 @@ class PostSerializer < BasicPostSerializer
post_bookmark&.name post_bookmark&.name
end end
def bookmark_delete_when_reminder_sent
post_bookmark&.delete_when_reminder_sent
end
def bookmark_id def bookmark_id
post_bookmark&.id post_bookmark&.id
end end

View File

@ -319,6 +319,7 @@ en:
no_timezone: 'You have not set a timezone yet. You will not be able to set reminders. Set one up <a href="%{basePath}/my/preferences/profile">in your profile</a>.' no_timezone: 'You have not set a timezone yet. You will not be able to set reminders. Set one up <a href="%{basePath}/my/preferences/profile">in your profile</a>.'
invalid_custom_datetime: "The date and time you provided is invalid, please try again." invalid_custom_datetime: "The date and time you provided is invalid, please try again."
list_permission_denied: "You do not have permission to view this user's bookmarks." list_permission_denied: "You do not have permission to view this user's bookmarks."
delete_when_reminder_sent: "Delete this bookmark when the reminder notification is sent."
reminders: reminders:
at_desktop: "Next time I'm at my desktop" at_desktop: "Next time I'm at my desktop"
later_today: "Later today" later_today: "Later today"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddDeleteWhenReminderSentBooleanToBookmarks < ActiveRecord::Migration[6.0]
def change
add_column :bookmarks, :delete_when_reminder_sent, :boolean, nullabe: false, default: false
end
end

View File

@ -7,13 +7,14 @@ class BookmarkManager
@user = user @user = user
end end
def create(post_id:, name: nil, reminder_type: nil, reminder_at: nil) def create(post_id:, name: nil, reminder_type: nil, reminder_at: nil, options: {})
post = Post.unscoped.includes(:topic).find(post_id) post = Post.unscoped.includes(:topic).find(post_id)
reminder_type = parse_reminder_type(reminder_type) reminder_type = parse_reminder_type(reminder_type)
raise Discourse::InvalidAccess.new if !Guardian.new(@user).can_see_post?(post) raise Discourse::InvalidAccess.new if !Guardian.new(@user).can_see_post?(post)
bookmark = Bookmark.create( bookmark = Bookmark.create(
{
user_id: @user.id, user_id: @user.id,
topic: post.topic, topic: post.topic,
post: post, post: post,
@ -21,6 +22,7 @@ class BookmarkManager
reminder_type: reminder_type, reminder_type: reminder_type,
reminder_at: reminder_at, reminder_at: reminder_at,
reminder_set_at: Time.zone.now reminder_set_at: Time.zone.now
}.merge(options)
) )
if bookmark.errors.any? if bookmark.errors.any?
@ -66,7 +68,7 @@ class BookmarkManager
BookmarkReminderNotificationHandler.send_notification(bookmark) BookmarkReminderNotificationHandler.send_notification(bookmark)
end end
def update(bookmark_id:, name:, reminder_type:, reminder_at:) def update(bookmark_id:, name:, reminder_type:, reminder_at:, options: {})
bookmark = Bookmark.find_by(id: bookmark_id) bookmark = Bookmark.find_by(id: bookmark_id)
raise Discourse::NotFound if bookmark.blank? raise Discourse::NotFound if bookmark.blank?
@ -75,10 +77,12 @@ class BookmarkManager
reminder_type = parse_reminder_type(reminder_type) reminder_type = parse_reminder_type(reminder_type)
success = bookmark.update( success = bookmark.update(
{
name: name, name: name,
reminder_at: reminder_at, reminder_at: reminder_at,
reminder_type: reminder_type, reminder_type: reminder_type,
reminder_set_at: Time.zone.now reminder_set_at: Time.zone.now
}.merge(options)
) )
if bookmark.errors.any? if bookmark.errors.any?

View File

@ -9,6 +9,11 @@ class BookmarkReminderNotificationHandler
end end
create_notification(bookmark) create_notification(bookmark)
if bookmark.delete_when_reminder_sent?
return bookmark.destroy
end
clear_reminder(bookmark) clear_reminder(bookmark)
end end
end end

View File

@ -42,6 +42,17 @@ RSpec.describe BookmarkManager do
end end
end end
context "when options are provided" do
let(:options) { { delete_when_reminder_sent: true } }
it "saves any additional options successfully" do
subject.create(post_id: post.id, name: name, options: options)
bookmark = Bookmark.find_by(user: user)
expect(bookmark.delete_when_reminder_sent).to eq(true)
end
end
context "when the bookmark already exists for the user & post" do context "when the bookmark already exists for the user & post" do
before do before do
Bookmark.create(post: post, user: user, topic: post.topic) Bookmark.create(post: post, user: user, topic: post.topic)
@ -142,14 +153,19 @@ RSpec.describe BookmarkManager do
let(:new_name) { "Some new name" } let(:new_name) { "Some new name" }
let(:new_reminder_at) { 10.days.from_now } let(:new_reminder_at) { 10.days.from_now }
let(:new_reminder_type) { Bookmark.reminder_types[:custom] } let(:new_reminder_type) { Bookmark.reminder_types[:custom] }
let(:options) { {} }
def update_bookmark def update_bookmark
subject.update( subject.update(
bookmark_id: bookmark.id, name: new_name, reminder_type: new_reminder_type, reminder_at: new_reminder_at bookmark_id: bookmark.id,
name: new_name,
reminder_type: new_reminder_type,
reminder_at: new_reminder_at,
options: options
) )
end end
it "saves the time and new reminder type sucessfully" do it "saves the time and new reminder type and new name sucessfully" do
update_bookmark update_bookmark
bookmark.reload bookmark.reload
expect(bookmark.name).to eq(new_name) expect(bookmark.name).to eq(new_name)
@ -157,6 +173,16 @@ RSpec.describe BookmarkManager do
expect(bookmark.reminder_type).to eq(new_reminder_type) expect(bookmark.reminder_type).to eq(new_reminder_type)
end end
context "when options are provided" do
let(:options) { { delete_when_reminder_sent: true } }
it "saves any additional options successfully" do
update_bookmark
bookmark.reload
expect(bookmark.delete_when_reminder_sent).to eq(true)
end
end
context "if the new reminder type is a string" do context "if the new reminder type is a string" do
let(:new_reminder_type) { "custom" } let(:new_reminder_type) { "custom" }
it "is parsed" do it "is parsed" do

View File

@ -28,6 +28,20 @@ RSpec.describe BookmarkReminderNotificationHandler do
expect(data["bookmark_name"]).to eq(bookmark.name) expect(data["bookmark_name"]).to eq(bookmark.name)
end end
it "clears the reminder" do
subject.send_notification(bookmark)
bookmark.reload
expect(bookmark.reload.no_reminder?).to eq(true)
end
context "when the delete_when_reminder_sent boolean is true " do
it "deletes the bookmark after the reminder gets sent" do
bookmark.update(delete_when_reminder_sent: true)
subject.send_notification(bookmark)
expect(Bookmark.find_by(id: bookmark.id)).to eq(nil)
end
end
context "when the post has been deleted" do context "when the post has been deleted" do
it "clears the reminder and does not send a notification" do it "clears the reminder and does not send a notification" do
bookmark.post.trash! bookmark.post.trash!

View File

@ -106,6 +106,30 @@ test("Saving a bookmark with a reminder", async assert => {
assert.verifySteps(["tomorrow"]); assert.verifySteps(["tomorrow"]);
}); });
test("Opening the options panel and remembering the option", async assert => {
mockSuccessfulBookmarkPost(assert);
await visit("/t/internationalization-localization/280");
await openBookmarkModal();
await click(".bookmark-options-button");
assert.ok(
exists(".bookmark-options-panel"),
"it should open the options panel"
);
await click("#delete_when_reminder_sent");
await click("#save-bookmark");
await openEditBookmarkModal();
assert.ok(
exists(".bookmark-options-panel"),
"it should reopen the options panel"
);
assert.ok(
exists(".bookmark-options-panel #delete_when_reminder_sent:checked"),
"it should pre-check delete when reminder sent option"
);
assert.verifySteps(["none"]);
});
test("Saving a bookmark with no reminder or name", async assert => { test("Saving a bookmark with no reminder or name", async assert => {
mockSuccessfulBookmarkPost(assert); mockSuccessfulBookmarkPost(assert);
await visit("/t/internationalization-localization/280"); await visit("/t/internationalization-localization/280");