FEATURE: Redesigned bookmark modal and menu (#23071)

Adds the new quick menu for bookmarking. When you bookmark
a post (chat message behaviour will come later) we show this new quick
menu and bookmark the item straight away.

You can then choose a reminder quick option, or choose Custom... to open
the old modal. If you click on an existing bookmark, we show the same quick menu
but with Edit and Delete options.

A later PR will introduce a new bookmark modal, but for now we
are using the old modal for Edit and Custom... options.
This commit is contained in:
Martin Brennan
2024-04-05 09:25:30 +10:00
committed by GitHub
parent 63594fa643
commit 67a8080e33
31 changed files with 825 additions and 135 deletions

View File

@ -4,7 +4,7 @@ import { service } from "@ember/service";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import BookmarkModal from "discourse/components/modal/bookmark"; import BookmarkModal from "discourse/components/modal/bookmark";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { BookmarkFormData } from "discourse/lib/bookmark"; import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
import { import {
openLinkInNewTab, openLinkInNewTab,
shouldOpenInNewTab, shouldOpenInNewTab,

View File

@ -0,0 +1,250 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { array, fn } from "@ember/helper";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import BookmarkModal from "discourse/components/modal/bookmark";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
TIME_SHORTCUT_TYPES,
timeShortcuts,
} from "discourse/lib/time-shortcut";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
export default class BookmarkMenu extends Component {
@service modal;
@service currentUser;
@service toasts;
@tracked quicksaved = false;
bookmarkManager = this.args.bookmarkManager;
timezone = this.currentUser?.user_option?.timezone || moment.tz.guess();
timeShortcuts = timeShortcuts(this.timezone);
@action
setReminderShortcuts() {
this.reminderAtOptions = [
this.timeShortcuts.twoHours(),
this.timeShortcuts.tomorrow(),
this.timeShortcuts.threeDays(),
];
// So the label is a simple 'Custom...'
const custom = this.timeShortcuts.custom();
custom.label = "time_shortcut.custom_short";
this.reminderAtOptions.push(custom);
}
get existingBookmark() {
return this.bookmarkManager.trackedBookmark.id
? this.bookmarkManager.trackedBookmark
: null;
}
get showEditDeleteMenu() {
return this.existingBookmark && !this.quicksaved;
}
get buttonTitle() {
if (!this.existingBookmark) {
return I18n.t("bookmarks.not_bookmarked");
} else {
if (this.existingBookmark.reminderAt) {
return I18n.t("bookmarks.created_with_reminder", {
date: this.existingBookmark.formattedReminder(this.timezone),
name: this.existingBookmark.name || "",
});
} else {
return I18n.t("bookmarks.created", {
name: this.existingBookmark.name || "",
});
}
}
}
@action
reminderShortcutTimeTitle(option) {
if (!option.time) {
return "";
}
return option.time.format(I18n.t(option.timeFormatKey));
}
@action
async onBookmark() {
try {
await this.bookmarkManager.create();
// We show the menu with Edit/Delete options if the bokmark exists,
// so this "quicksave" will do nothing in that case.
// NOTE: Need a nicer way to handle this; otherwise as soon as you save
// a bookmark, it switches to the other Edit/Delete menu.
this.quicksaved = true;
this.toasts.success({
duration: 3000,
data: { message: I18n.t("bookmarks.bookmarked_success") },
});
} catch (error) {
popupAjaxError(error);
}
}
@action
onShowMenu() {
if (!this.existingBookmark) {
this.onBookmark();
}
}
@action
onRegisterApi(api) {
this.dMenu = api;
}
@action
onEditBookmark() {
this._openBookmarkModal();
}
@action
onCloseMenu() {
this.quicksaved = false;
}
@action
async onRemoveBookmark() {
try {
const response = await this.bookmarkManager.delete();
this.bookmarkManager.afterDelete(response, this.existingBookmark.id);
this.toasts.success({
duration: 3000,
data: { message: I18n.t("bookmarks.deleted_bookmark_success") },
});
} catch (error) {
popupAjaxError(error);
} finally {
this.dMenu.close();
}
}
@action
async onChooseReminderOption(option) {
if (option.id === TIME_SHORTCUT_TYPES.CUSTOM) {
this._openBookmarkModal();
} else {
this.existingBookmark.selectedReminderType = option.id;
this.existingBookmark.selectedDatetime = option.time;
this.existingBookmark.reminderAt = option.time;
try {
await this.bookmarkManager.save();
this.toasts.success({
duration: 3000,
data: { message: I18n.t("bookmarks.reminder_set_success") },
});
} catch (error) {
popupAjaxError(error);
} finally {
this.dMenu.close();
}
}
}
async _openBookmarkModal() {
try {
const closeData = await this.modal.show(BookmarkModal, {
model: {
bookmark: this.existingBookmark,
afterSave: (savedData) => {
return this.bookmarkManager.afterSave(savedData);
},
afterDelete: (response, bookmarkId) => {
this.bookmarkManager.afterDelete(response, bookmarkId);
},
},
});
this.bookmarkManager.afterModalClose(closeData);
} catch (error) {
popupAjaxError(error);
}
}
<template>
<DMenu
{{didInsert this.setReminderShortcuts}}
@identifier="bookmark-menu"
@triggers={{array "click"}}
@arrow="true"
class={{concatClass
"bookmark widget-button btn-flat no-text btn-icon bookmark-menu__trigger"
(if this.existingBookmark "bookmarked")
(if this.existingBookmark.reminderAt "with-reminder")
}}
@title={{this.buttonTitle}}
@onClose={{this.onCloseMenu}}
@onShow={{this.onShowMenu}}
@onRegisterApi={{this.onRegisterApi}}
>
<:trigger>
{{#if this.existingBookmark.reminderAt}}
{{icon "discourse-bookmark-clock"}}
{{else}}
{{icon "bookmark"}}
{{/if}}
</:trigger>
<:content>
<div class="bookmark-menu__body">
{{#if this.showEditDeleteMenu}}
<ul class="bookmark-menu__actions">
<li class="bookmark-menu__row -edit" data-menu-option-id="edit">
<DButton
@icon="pencil-alt"
@label="edit"
@action={{this.onEditBookmark}}
@class="bookmark-menu__row-btn btn-flat"
/>
</li>
<li
class="bookmark-menu__row -remove"
role="button"
tabindex="0"
data-menu-option-id="delete"
>
<DButton
@icon="trash-alt"
@label="delete"
@action={{this.onRemoveBookmark}}
@class="bookmark-menu__row-btn btn-flat"
/>
</li>
</ul>
{{else}}
<span class="bookmark-menu__row-title">{{i18n
"bookmarks.also_set_reminder"
}}</span>
<ul class="bookmark-menu__actions">
{{#each this.reminderAtOptions as |option|}}
<li
class="bookmark-menu__row"
data-menu-option-id={{option.id}}
>
<DButton
@label={{option.label}}
@translatedTitle={{this.reminderShortcutTimeTitle option}}
@action={{fn this.onChooseReminderOption option}}
@class="bookmark-menu__row-btn btn-flat"
/>
</li>
{{/each}}
</ul>
{{/if}}
</div>
</:content>
</DMenu>
</template>
}

View File

@ -4,7 +4,7 @@
@flash={{this.flash}} @flash={{this.flash}}
@flashType="error" @flashType="error"
id="bookmark-reminder-modal" id="bookmark-reminder-modal"
class="bookmark-reminder-modal bookmark-with-reminder" class="bookmark-reminder-modal"
data-bookmark-id={{this.bookmark.id}} data-bookmark-id={{this.bookmark.id}}
{{did-insert this.didInsert}} {{did-insert this.didInsert}}
> >

View File

@ -6,7 +6,6 @@ import { service } from "@ember/service";
import ItsATrap from "@discourse/itsatrap"; import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import { CLOSE_INITIATED_BY_CLICK_OUTSIDE } from "discourse/components/d-modal"; import { CLOSE_INITIATED_BY_CLICK_OUTSIDE } from "discourse/components/d-modal";
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error"; import { extractError } from "discourse/lib/ajax-error";
import { formattedReminderTime } from "discourse/lib/bookmark"; import { formattedReminderTime } from "discourse/lib/bookmark";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
@ -29,6 +28,7 @@ export default class BookmarkModal extends Component {
@service dialog; @service dialog;
@service currentUser; @service currentUser;
@service site; @service site;
@service bookmarkApi;
@tracked postDetectedLocalDate = null; @tracked postDetectedLocalDate = null;
@tracked postDetectedLocalTime = null; @tracked postDetectedLocalTime = null;
@ -264,31 +264,19 @@ export default class BookmarkModal extends Component {
} }
if (this.editingExistingBookmark) { if (this.editingExistingBookmark) {
return ajax(`/bookmarks/${this.bookmark.id}`, { return this.bookmarkApi.update(this.bookmark).then(() => {
type: "PUT", this.args.model.afterSave?.(this.bookmark);
data: this.bookmark.saveData,
}).then(() => {
this.args.model.afterSave?.(this.bookmark.saveData);
}); });
} else { } else {
return ajax("/bookmarks", { return this.bookmarkApi.create(this.bookmark).then(() => {
type: "POST", this.args.model.afterSave?.(this.bookmark);
data: this.bookmark.saveData,
}).then((response) => {
this.bookmark.id = response.id;
this.args.model.afterSave?.(this.bookmark.saveData);
}); });
} }
} }
#deleteBookmark() { #deleteBookmark() {
return ajax("/bookmarks/" + this.bookmark.id, { return this.bookmarkApi.delete(this.bookmark.id).then((response) => {
type: "DELETE", this.args.model.afterDelete?.(response, this.bookmark.id);
}).then((response) => {
this.args.model.afterDelete?.(
response.topic_bookmarked,
this.bookmark.id
);
}); });
} }

View File

@ -17,7 +17,7 @@ import JumpToPost from "discourse/components/modal/jump-to-post";
import { spinnerHTML } from "discourse/helpers/loading-spinner"; import { spinnerHTML } from "discourse/helpers/loading-spinner";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { BookmarkFormData } from "discourse/lib/bookmark"; import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
import { resetCachedTopicList } from "discourse/lib/cached-topic-list"; import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
import { buildQuote } from "discourse/lib/quote"; import { buildQuote } from "discourse/lib/quote";
import QuoteState from "discourse/lib/quote-state"; import QuoteState from "discourse/lib/quote-state";
@ -1281,14 +1281,14 @@ export default Controller.extend(bufferedProperty("model"), {
this.modal.show(BookmarkModal, { this.modal.show(BookmarkModal, {
model: { model: {
bookmark: new BookmarkFormData(bookmark), bookmark: new BookmarkFormData(bookmark),
afterSave: (savedData) => { afterSave: (bookmarkFormData) => {
this._syncBookmarks(savedData); this._syncBookmarks(bookmarkFormData.saveData);
this.model.set("bookmarking", false); this.model.set("bookmarking", false);
this.model.set("bookmarked", true); this.model.set("bookmarked", true);
this.model.incrementProperty("bookmarksWereChanged"); this.model.incrementProperty("bookmarksWereChanged");
this.appEvents.trigger( this.appEvents.trigger(
"bookmarks:changed", "bookmarks:changed",
savedData, bookmarkFormData.saveData,
bookmark.attachedTo() bookmark.attachedTo()
); );
}, },

View File

@ -0,0 +1,28 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import PostBookmarkManager from "discourse/lib/post-bookmark-manager";
export default {
name: "discourse-bookmark-menu",
initialize(container) {
const currentUser = container.lookup("service:current-user");
withPluginApi("0.10.1", (api) => {
if (currentUser) {
api.replacePostMenuButton("bookmark", {
name: "bookmark-menu-shim",
shouldRender: () => true,
buildAttrs: (widget) => {
return {
post: widget.findAncestorModel(),
bookmarkManager: new PostBookmarkManager(
container,
widget.findAncestorModel()
),
};
},
});
}
});
},
};

View File

@ -0,0 +1,56 @@
import { tracked } from "@glimmer/tracking";
import { formattedReminderTime } from "discourse/lib/bookmark";
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
export class BookmarkFormData {
@tracked selectedDatetime;
@tracked selectedReminderType = TIME_SHORTCUT_TYPES.NONE;
@tracked id;
@tracked reminderAt;
@tracked autoDeletePreference;
@tracked name;
@tracked bookmarkableId;
@tracked bookmarkableType;
constructor(bookmark) {
this.id = bookmark.id;
this.reminderAt = bookmark.reminder_at;
this.name = bookmark.name;
this.bookmarkableId = bookmark.bookmarkable_id;
this.bookmarkableType = bookmark.bookmarkable_type;
this.autoDeletePreference =
bookmark.auto_delete_preference ?? AUTO_DELETE_PREFERENCES.CLEAR_REMINDER;
}
get reminderAtISO() {
if (this.selectedReminderType === TIME_SHORTCUT_TYPES.NONE) {
return null;
}
if (!this.selectedReminderType || !this.selectedDatetime) {
if (this.reminderAt) {
return this.reminderAt.toISOString();
} else {
return null;
}
}
return this.selectedDatetime.toISOString();
}
get saveData() {
return {
reminder_at: this.reminderAtISO,
name: this.name,
id: this.id,
auto_delete_preference: this.autoDeletePreference,
bookmarkable_id: this.bookmarkableId,
bookmarkable_type: this.bookmarkableType,
};
}
formattedReminder(timezone) {
return formattedReminderTime(this.reminderAt, timezone);
}
}

View File

@ -1,6 +1,3 @@
import { tracked } from "@glimmer/tracking";
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
export function formattedReminderTime(reminderAt, timezone) { export function formattedReminderTime(reminderAt, timezone) {
@ -20,43 +17,3 @@ export function formattedReminderTime(reminderAt, timezone) {
date_time: reminderAtDate.format(I18n.t("dates.long_with_year")), date_time: reminderAtDate.format(I18n.t("dates.long_with_year")),
}); });
} }
export class BookmarkFormData {
@tracked selectedDatetime;
@tracked selectedReminderType = TIME_SHORTCUT_TYPES.NONE;
@tracked id;
@tracked reminderAt;
@tracked autoDeletePreference;
@tracked name;
@tracked bookmarkableId;
@tracked bookmarkableType;
constructor(bookmark) {
this.id = bookmark.id;
this.reminderAt = bookmark.reminder_at;
this.name = bookmark.name;
this.bookmarkableId = bookmark.bookmarkable_id;
this.bookmarkableType = bookmark.bookmarkable_type;
this.autoDeletePreference =
bookmark.auto_delete_preference ?? AUTO_DELETE_PREFERENCES.CLEAR_REMINDER;
}
get reminderAtISO() {
if (!this.selectedReminderType || !this.selectedDatetime) {
return;
}
return this.selectedDatetime.toISOString();
}
get saveData() {
return {
reminder_at: this.reminderAtISO,
name: this.name,
id: this.id,
auto_delete_preference: this.autoDeletePreference,
bookmarkable_id: this.bookmarkableId,
bookmarkable_type: this.bookmarkableType,
};
}
}

View File

@ -0,0 +1,102 @@
import { tracked } from "@glimmer/tracking";
import { setOwner } from "@ember/application";
import { inject as controller } from "@ember/controller";
import { inject as service } from "@ember/service";
import {
CLOSE_INITIATED_BY_BUTTON,
CLOSE_INITIATED_BY_ESC,
} from "discourse/components/d-modal";
import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
import Bookmark from "discourse/models/bookmark";
export default class PostBookmarkManager {
@service currentUser;
@service bookmarkApi;
@controller("topic") topicController;
@tracked trackedBookmark;
@tracked bookmarkModel;
constructor(owner, post) {
setOwner(this, owner);
this.model = post;
this.type = "Post";
this.bookmarkModel =
this.topicController.model?.bookmarks.find(
(bookmark) =>
bookmark.bookmarkable_id === this.model.id &&
bookmark.bookmarkable_type === this.type
) || this.bookmarkApi.buildNewBookmark(this.type, this.model.id);
this.trackedBookmark = new BookmarkFormData(this.bookmarkModel);
}
create() {
return this.bookmarkApi
.create(this.trackedBookmark)
.then((updatedBookmark) => {
this.trackedBookmark = updatedBookmark;
});
}
delete() {
return this.bookmarkApi.delete(this.trackedBookmark.id);
}
save() {
return this.bookmarkApi.update(this.trackedBookmark);
}
afterModalClose(closeData) {
if (!closeData) {
return;
}
if (
closeData.closeWithoutSaving ||
closeData.initiatedBy === CLOSE_INITIATED_BY_ESC ||
closeData.initiatedBy === CLOSE_INITIATED_BY_BUTTON
) {
this.model.appEvents.trigger("post-stream:refresh", {
id: this.model.id,
});
}
}
afterSave(bookmarkFormData) {
this.trackedBookmark = bookmarkFormData;
this._syncBookmarks(bookmarkFormData.saveData);
this.topicController.model.set("bookmarking", false);
this.model.createBookmark(bookmarkFormData.saveData);
this.topicController.model.afterPostBookmarked(
this.model,
bookmarkFormData.saveData
);
return [this.model.id];
}
afterDelete(deleteResponse, bookmarkId) {
this.topicController.model.removeBookmark(bookmarkId);
this.model.deleteBookmark(deleteResponse.topic_bookmarked);
this.bookmarkModel = this.bookmarkApi.buildNewBookmark(
this.type,
this.model.id
);
this.trackedBookmark = new BookmarkFormData(this.bookmarkModel);
}
_syncBookmarks(data) {
if (!this.topicController.bookmarks) {
this.topicController.set("bookmarks", []);
}
const bookmark = this.topicController.bookmarks.findBy("id", data.id);
if (!bookmark) {
this.topicController.bookmarks.pushObject(Bookmark.create(data));
} else {
bookmark.reminder_at = data.reminder_at;
bookmark.name = data.name;
bookmark.auto_delete_preference = data.auto_delete_preference;
}
}
}

View File

@ -1,5 +1,6 @@
import { import {
fourMonths, fourMonths,
inNDays,
LATER_TODAY_CUTOFF_HOUR, LATER_TODAY_CUTOFF_HOUR,
laterThisWeek, laterThisWeek,
laterToday, laterToday,
@ -126,6 +127,15 @@ export function timeShortcuts(timezone) {
timeFormatKey: "dates.time_short_day", timeFormatKey: "dates.time_short_day",
}; };
}, },
threeDays() {
return {
id: "three_days",
icon: "angle-right",
label: "time_shortcut.three_days",
time: inNDays(timezone, 3),
timeFormatKey: "dates.time_short_day",
};
},
laterThisWeek() { laterThisWeek() {
return { return {
id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,

View File

@ -49,6 +49,10 @@ export function twoDays(timezone) {
return startOfDay(now(timezone).add(2, "days")); return startOfDay(now(timezone).add(2, "days"));
} }
export function inNDays(timezone, num) {
return startOfDay(now(timezone).add(num, "days"));
}
export function laterThisWeek(timezone) { export function laterThisWeek(timezone) {
return twoDays(timezone); return twoDays(timezone);
} }

View File

@ -0,0 +1,41 @@
import Service, { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Bookmark from "discourse/models/bookmark";
export default class BookmarkApi extends Service {
@service currentUser;
buildNewBookmark(bookmarkableType, bookmarkableId) {
return Bookmark.createFor(
this.currentUser,
bookmarkableType,
bookmarkableId
);
}
create(bookmarkFormData) {
return ajax("/bookmarks.json", {
method: "POST",
data: bookmarkFormData.saveData,
})
.then((response) => {
bookmarkFormData.id = response.id;
return bookmarkFormData;
})
.catch(popupAjaxError);
}
delete(bookmarkId) {
return ajax(`/bookmarks/${bookmarkId}.json`, {
method: "DELETE",
}).catch(popupAjaxError);
}
update(bookmarkFormData) {
return ajax(`/bookmarks/${bookmarkFormData.id}.json`, {
method: "PUT",
data: bookmarkFormData.saveData,
}).catch(popupAjaxError);
}
}

View File

@ -155,7 +155,7 @@
href href
{{on "click" this.editTopic}} {{on "click" this.editTopic}}
class="edit-topic" class="edit-topic"
title={{i18n "edit"}} title={{i18n "edit_topic"}}
>{{d-icon "pencil-alt"}}</a> >{{d-icon "pencil-alt"}}</a>
{{/if}} {{/if}}

View File

@ -0,0 +1,8 @@
import { hbs } from "ember-cli-htmlbars";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
registerWidgetShim(
"bookmark-menu-shim",
"div.bookmark-menu-shim",
hbs`<BookmarkMenu @bookmarkManager={{@data.bookmarkManager}} />`
);

View File

@ -387,7 +387,7 @@ registerButton(
return; return;
} }
let classNames = ["bookmark", "with-reminder"]; let classNames = ["bookmark"];
let title = "bookmarks.not_bookmarked"; let title = "bookmarks.not_bookmarked";
let titleOptions = { name: "" }; let titleOptions = { name: "" };
@ -395,6 +395,8 @@ registerButton(
classNames.push("bookmarked"); classNames.push("bookmarked");
if (attrs.bookmarkReminderAt) { if (attrs.bookmarkReminderAt) {
classNames.push("with-reminder");
let formattedReminder = formattedReminderTime( let formattedReminder = formattedReminderTime(
attrs.bookmarkReminderAt, attrs.bookmarkReminderAt,
currentUser.user_option.timezone currentUser.user_option.timezone

View File

@ -9,6 +9,7 @@ import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import DDefaultToast from "float-kit/components/d-default-toast"; import DDefaultToast from "float-kit/components/d-default-toast";
import DMenuInstance from "float-kit/lib/d-menu-instance";
module("Integration | Component | FloatKit | d-menu", function (hooks) { module("Integration | Component | FloatKit | d-menu", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -17,6 +18,10 @@ module("Integration | Component | FloatKit | d-menu", function (hooks) {
await triggerEvent(".fk-d-menu__trigger", "click"); await triggerEvent(".fk-d-menu__trigger", "click");
} }
async function close() {
await triggerEvent(".fk-d-menu__trigger.-expanded", "click");
}
test("@label", async function (assert) { test("@label", async function (assert) {
await render(hbs`<DMenu @inline={{true}} @label="label" />`); await render(hbs`<DMenu @inline={{true}} @label="label" />`);
@ -38,6 +43,38 @@ module("Integration | Component | FloatKit | d-menu", function (hooks) {
assert.dom(".fk-d-menu").hasText("content"); assert.dom(".fk-d-menu").hasText("content");
}); });
test("@onRegisterApi", async function (assert) {
this.api = null;
this.onRegisterApi = (api) => (this.api = api);
await render(
hbs`<DMenu @inline={{true}} @onRegisterApi={{this.onRegisterApi}} />`
);
assert.ok(this.api instanceof DMenuInstance);
});
test("@onShow", async function (assert) {
this.test = false;
this.onShow = () => (this.test = true);
await render(hbs`<DMenu @inline={{true}} @onShow={{this.onShow}} />`);
await open();
assert.strictEqual(this.test, true);
});
test("@onClose", async function (assert) {
this.test = false;
this.onClose = () => (this.test = true);
await render(hbs`<DMenu @inline={{true}} @onClose={{this.onClose}} />`);
await open();
await close();
assert.strictEqual(this.test, true);
});
test("-expanded class", async function (assert) { test("-expanded class", async function (assert) {
await render(hbs`<DMenu @inline={{true}} @label="label" />`); await render(hbs`<DMenu @inline={{true}} @label="label" />`);

View File

@ -9,6 +9,7 @@ import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import DDefaultToast from "float-kit/components/d-default-toast"; import DDefaultToast from "float-kit/components/d-default-toast";
import DTooltipInstance from "float-kit/lib/d-tooltip-instance";
module("Integration | Component | FloatKit | d-tooltip", function (hooks) { module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -17,6 +18,10 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
await triggerEvent(".fk-d-tooltip__trigger", "mousemove"); await triggerEvent(".fk-d-tooltip__trigger", "mousemove");
} }
async function close() {
await triggerKeyEvent(document.activeElement, "keydown", "Escape");
}
test("@label", async function (assert) { test("@label", async function (assert) {
await render(hbs`<DTooltip @inline={{true}} @label="label" />`); await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
@ -38,6 +43,39 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
assert.dom(".fk-d-tooltip").hasText("content"); assert.dom(".fk-d-tooltip").hasText("content");
}); });
test("@onRegisterApi", async function (assert) {
this.api = null;
this.onRegisterApi = (api) => (this.api = api);
await render(
hbs`<DTooltip @inline={{true}} @onRegisterApi={{this.onRegisterApi}} />`
);
assert.ok(this.api instanceof DTooltipInstance);
});
test("@onShow", async function (assert) {
this.test = false;
this.onShow = () => (this.test = true);
await render(hbs`<DTooltip @inline={{true}} @onShow={{this.onShow}} />`);
await hover();
assert.strictEqual(this.test, true);
});
test("@onClose", async function (assert) {
this.test = false;
this.onClose = () => (this.test = true);
await render(hbs`<DTooltip @inline={{true}} @onClose={{this.onClose}} />`);
await hover();
await close();
assert.strictEqual(this.test, true);
});
test("-expanded class", async function (assert) { test("-expanded class", async function (assert) {
await render(hbs`<DTooltip @inline={{true}} @label="label" />`); await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
@ -140,7 +178,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{true}} />` hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{true}} />`
); );
await hover(); await hover();
await triggerKeyEvent(document.activeElement, "keydown", "Escape"); await close();
assert.dom(".fk-d-tooltip").doesNotExist(); assert.dom(".fk-d-tooltip").doesNotExist();
@ -148,7 +186,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{false}} />` hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{false}} />`
); );
await hover(); await hover();
await triggerKeyEvent(document.activeElement, "keydown", "Escape"); await close();
assert.dom(".fk-d-tooltip").exists(); assert.dom(".fk-d-tooltip").exists();
}); });

View File

@ -1,11 +1,14 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { getOwner } from "@ember/application"; import { getOwner } from "@ember/application";
import { service } from "@ember/service"; import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { modifier } from "ember-modifier"; import { modifier } from "ember-modifier";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import DFloatBody from "float-kit/components/d-float-body"; import DFloatBody from "float-kit/components/d-float-body";
import { MENU } from "float-kit/lib/constants";
import DMenuInstance from "float-kit/lib/d-menu-instance"; import DMenuInstance from "float-kit/lib/d-menu-instance";
export default class DMenu extends Component { export default class DMenu extends Component {
@ -13,9 +16,9 @@ export default class DMenu extends Component {
@tracked menuInstance = null; @tracked menuInstance = null;
registerTrigger = modifier((element) => { registerTrigger = modifier((element, [properties]) => {
const options = { const options = {
...this.args, ...properties,
...{ ...{
autoUpdate: true, autoUpdate: true,
listeners: true, listeners: true,
@ -28,6 +31,8 @@ export default class DMenu extends Component {
this.menuInstance = instance; this.menuInstance = instance;
this.options.onRegisterApi?.(this.menuInstance);
return () => { return () => {
instance.destroy(); instance.destroy();
@ -52,11 +57,22 @@ export default class DMenu extends Component {
}; };
} }
@action
allowedProperties() {
const properties = {};
Object.keys(MENU.options).forEach((key) => {
const value = MENU.options[key];
properties[key] = this.args[key] ?? value;
});
return properties;
}
<template> <template>
<DButton <DButton
class={{concatClass class={{concatClass
"fk-d-menu__trigger" "fk-d-menu__trigger"
(if this.menuInstance.expanded "-expanded") (if this.menuInstance.expanded "-expanded")
(concat this.options.identifier "-trigger")
}} }}
id={{this.menuInstance.id}} id={{this.menuInstance.id}}
data-identifier={{this.options.identifier}} data-identifier={{this.options.identifier}}
@ -67,7 +83,7 @@ export default class DMenu extends Component {
@translatedTitle={{@title}} @translatedTitle={{@title}}
@disabled={{@disabled}} @disabled={{@disabled}}
aria-expanded={{if this.menuInstance.expanded "true" "false"}} aria-expanded={{if this.menuInstance.expanded "true" "false"}}
{{this.registerTrigger}} {{this.registerTrigger (this.allowedProperties)}}
...attributes ...attributes
> >
{{#if (has-block "trigger")}} {{#if (has-block "trigger")}}
@ -79,7 +95,10 @@ export default class DMenu extends Component {
<DFloatBody <DFloatBody
@instance={{this.menuInstance}} @instance={{this.menuInstance}}
@trapTab={{this.options.trapTab}} @trapTab={{this.options.trapTab}}
@mainClass="fk-d-menu" @mainClass={{concatClass
"fk-d-menu"
(concat this.options.identifier "-content")
}}
@innerClass="fk-d-menu__inner-content" @innerClass="fk-d-menu__inner-content"
@role="dialog" @role="dialog"
@inline={{this.options.inline}} @inline={{this.options.inline}}

View File

@ -1,12 +1,14 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { getOwner } from "@ember/application"; import { getOwner } from "@ember/application";
import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { modifier } from "ember-modifier"; import { modifier } from "ember-modifier";
import { and } from "truth-helpers"; import { and } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import DFloatBody from "float-kit/components/d-float-body"; import DFloatBody from "float-kit/components/d-float-body";
import { TOOLTIP } from "float-kit/lib/constants";
import DTooltipInstance from "float-kit/lib/d-tooltip-instance"; import DTooltipInstance from "float-kit/lib/d-tooltip-instance";
export default class DTooltip extends Component { export default class DTooltip extends Component {
@ -15,9 +17,9 @@ export default class DTooltip extends Component {
@tracked tooltipInstance = null; @tracked tooltipInstance = null;
registerTrigger = modifier((element) => { registerTrigger = modifier((element, [properties]) => {
const options = { const options = {
...this.args, ...properties,
...{ ...{
listeners: true, listeners: true,
beforeTrigger: (instance) => { beforeTrigger: (instance) => {
@ -30,6 +32,8 @@ export default class DTooltip extends Component {
this.tooltipInstance = instance; this.tooltipInstance = instance;
this.options.onRegisterApi?.(instance);
return () => { return () => {
instance.destroy(); instance.destroy();
@ -50,6 +54,16 @@ export default class DTooltip extends Component {
}; };
} }
@action
allowedProperties() {
const properties = {};
Object.keys(TOOLTIP.options).forEach((key) => {
const value = TOOLTIP.options[key];
properties[key] = this.args[key] ?? value;
});
return properties;
}
<template> <template>
<span <span
class={{concatClass class={{concatClass
@ -61,7 +75,7 @@ export default class DTooltip extends Component {
data-identifier={{this.options.identifier}} data-identifier={{this.options.identifier}}
data-trigger data-trigger
aria-expanded={{if this.tooltipInstance.expanded "true" "false"}} aria-expanded={{if this.tooltipInstance.expanded "true" "false"}}
{{this.registerTrigger}} {{this.registerTrigger (this.allowedProperties)}}
...attributes ...attributes
> >
<div class="fk-d-tooltip__trigger-container"> <div class="fk-d-tooltip__trigger-container">

View File

@ -35,6 +35,9 @@ export const TOOLTIP = {
fallbackPlacements: FLOAT_UI_PLACEMENTS, fallbackPlacements: FLOAT_UI_PLACEMENTS,
autoUpdate: true, autoUpdate: true,
trapTab: true, trapTab: true,
onClose: null,
onShow: null,
onRegisterApi: null,
}, },
portalOutletId: "d-tooltip-portal-outlet", portalOutletId: "d-tooltip-portal-outlet",
}; };
@ -62,6 +65,9 @@ export const MENU = {
autoUpdate: true, autoUpdate: true,
trapTab: true, trapTab: true,
extraClassName: null, extraClassName: null,
onClose: null,
onShow: null,
onRegisterApi: null,
}, },
portalOutletId: "d-menu-portal-outlet", portalOutletId: "d-menu-portal-outlet",
}; };

View File

@ -1,6 +1,6 @@
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { cancel } from "@ember/runloop"; import { cancel, next } from "@ember/runloop";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
@ -22,11 +22,19 @@ export default class FloatKitInstance {
@action @action
show() { show() {
this.expanded = true; this.expanded = true;
next(() => {
this.options.onShow?.();
});
} }
@action @action
close() { close() {
this.expanded = false; this.expanded = false;
next(() => {
this.options.onClose?.();
});
} }
@action @action

View File

@ -2,6 +2,7 @@
@import "banner"; @import "banner";
@import "bookmark-list"; @import "bookmark-list";
@import "bookmark-modal"; @import "bookmark-modal";
@import "bookmark-menu";
@import "buttons"; @import "buttons";
@import "color-input"; @import "color-input";
@import "char-counter"; @import "char-counter";

View File

@ -0,0 +1,91 @@
.bookmark-menu-content {
.bookmark-menu__body {
background: var(--secondary);
list-style: none;
display: flex;
flex-direction: column;
color: var(--primary);
.bookmark-menu__actions {
margin: 0;
padding: 0;
list-style: none;
}
}
.bookmark-menu {
&__text {
display: flex;
align-items: left;
}
&__row {
border-bottom: 1px solid var(--primary-low);
width: 100%;
display: flex;
align-items: left;
&:hover,
&:focus {
background: var(--tertiary-very-low);
}
&-title {
font-size: var(--font-down-1);
padding: 0.75rem;
border-bottom: 1px solid var(--primary-low);
}
&-btn {
margin: 0;
padding: 0.75rem;
width: 100%;
text-align: left;
justify-content: left;
.d-icon {
color: var(--primary);
}
.d-button-label {
color: var(--primary);
font-size: var(--font-down-1);
}
&:hover,
&:focus {
background: var(--tertiary-very-low);
}
}
&.-edit {
.d-icon {
margin-right: 5px;
}
}
&.-remove {
.d-icon {
color: var(--danger);
}
&:hover,
&:focus {
background: var(--danger-low);
}
}
&:last-child {
border-bottom: none;
}
&.-no-reminder {
border-bottom: 2px solid var(--primary-low);
}
}
}
.bookmark-menu__row-title {
font-weight: 900;
padding: 0.75rem;
}
}

View File

@ -73,7 +73,6 @@ class CurrentUserSerializer < BasicUserSerializer
:sidebar_sections, :sidebar_sections,
:new_new_view_enabled?, :new_new_view_enabled?,
:use_experimental_topic_bulk_actions?, :use_experimental_topic_bulk_actions?,
:use_experimental_topic_bulk_actions?,
:use_admin_sidebar, :use_admin_sidebar,
:glimmer_header_enabled?, :glimmer_header_enabled?,
:can_view_raw_email :can_view_raw_email

View File

@ -259,7 +259,8 @@ en:
us_west_2: "US West (Oregon)" us_west_2: "US West (Oregon)"
clear_input: "Clear input" clear_input: "Clear input"
edit: "edit the title and category of this topic" edit: "Edit"
edit_topic: "edit the title and category of this topic"
expand: "Expand" expand: "Expand"
not_implemented: "That feature hasn't been implemented yet, sorry!" not_implemented: "That feature hasn't been implemented yet, sorry!"
no_value: "No" no_value: "No"
@ -353,6 +354,10 @@ en:
unbookmark_with_reminder: "Click to remove all bookmarks and reminders in this topic" unbookmark_with_reminder: "Click to remove all bookmarks and reminders in this topic"
bookmarks: bookmarks:
also_set_reminder: "Also set a reminder?"
bookmarked_success: "Bookmarked!"
deleted_bookmark_success: "Bookmark deleted!"
reminder_set_success: "Reminder has beeen set!"
created: "You've bookmarked this post. %{name}" created: "You've bookmarked this post. %{name}"
created_generic: "You've bookmarked this. %{name}" created_generic: "You've bookmarked this. %{name}"
create: "Create bookmark" create: "Create bookmark"
@ -375,8 +380,11 @@ en:
when_reminder_sent: "Delete bookmark" when_reminder_sent: "Delete bookmark"
on_owner_reply: "Delete bookmark, once I reply" on_owner_reply: "Delete bookmark, once I reply"
clear_reminder: "Keep bookmark and clear reminder" clear_reminder: "Keep bookmark and clear reminder"
after_reminder_label: "After reminding you we should..."
after_reminder_checkbox: "Set this as default for all future bookmark reminders"
search_placeholder: "Search bookmarks by name, topic title, or post content" search_placeholder: "Search bookmarks by name, topic title, or post content"
search: "Search" search: "Search"
bookmark: "Bookmark"
reminders: reminders:
today_with_time: "today at %{time}" today_with_time: "today at %{time}"
tomorrow_with_time: "tomorrow at %{time}" tomorrow_with_time: "tomorrow at %{time}"
@ -700,6 +708,7 @@ en:
in_two_hours: "In two hours" in_two_hours: "In two hours"
later_today: "Later today" later_today: "Later today"
two_days: "Two days" two_days: "Two days"
three_days: "In three days"
next_business_day: "Next business day" next_business_day: "Next business day"
tomorrow: "Tomorrow" tomorrow: "Tomorrow"
post_local_date: "Date in post" post_local_date: "Date in post"
@ -721,6 +730,7 @@ en:
never: "Never" never: "Never"
last_custom: "Last custom datetime" last_custom: "Last custom datetime"
custom: "Custom date and time" custom: "Custom date and time"
custom_short: "Custom..."
select_timeframe: "Select a timeframe" select_timeframe: "Select a timeframe"
user_action: user_action:

View File

@ -2617,8 +2617,8 @@ en:
enable_custom_sidebar_sections: "EXPERIMENTAL: Enable custom sidebar sections" enable_custom_sidebar_sections: "EXPERIMENTAL: Enable custom sidebar sections"
experimental_topics_filter: "EXPERIMENTAL: Enables the experimental topics filter route at /filter" experimental_topics_filter: "EXPERIMENTAL: Enables the experimental topics filter route at /filter"
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design." enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
enable_experimental_bookmark_redesign_groups: "EXPERIMENTAL: Show a quick access menu for bookmarks on posts and a new redesigned modal"
experimental_glimmer_header_groups: "EXPERIMENTAL: Render the site header as glimmer components." experimental_glimmer_header_groups: "EXPERIMENTAL: Render the site header as glimmer components."
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>." experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons." admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories." lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."

View File

@ -5,7 +5,7 @@ import { service } from "@ember/service";
import BookmarkModal from "discourse/components/modal/bookmark"; import BookmarkModal from "discourse/components/modal/bookmark";
import FlagModal from "discourse/components/modal/flag"; import FlagModal from "discourse/components/modal/flag";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { BookmarkFormData } from "discourse/lib/bookmark"; import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
import { clipboardCopy } from "discourse/lib/utilities"; import { clipboardCopy } from "discourse/lib/utilities";
import Bookmark from "discourse/models/bookmark"; import Bookmark from "discourse/models/bookmark";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
@ -337,12 +337,12 @@ export default class ChatMessageInteractor {
this.message.id this.message.id
) )
), ),
afterSave: (savedData) => { afterSave: (bookmarkFormData) => {
const bookmark = Bookmark.create(savedData); const bookmark = Bookmark.create(bookmarkFormData.saveData);
this.message.bookmark = bookmark; this.message.bookmark = bookmark;
this.appEvents.trigger( this.appEvents.trigger(
"bookmarks:changed", "bookmarks:changed",
savedData, bookmarkFormData.saveData,
bookmark.attachedTo() bookmark.attachedTo()
); );
}, },

View File

@ -6,6 +6,7 @@ describe "Local dates", type: :system do
let(:year) { Time.zone.now.year + 1 } let(:year) { Time.zone.now.year + 1 }
let(:month) { Time.zone.now.month } let(:month) { Time.zone.now.month }
let(:bookmark_modal) { PageObjects::Modals::Bookmark.new } let(:bookmark_modal) { PageObjects::Modals::Bookmark.new }
let(:bookmark_menu) { PageObjects::Components::BookmarkMenu.new }
let(:composer) { PageObjects::Components::Composer.new } let(:composer) { PageObjects::Components::Composer.new }
let(:insert_datetime_modal) { PageObjects::Modals::InsertDateTime.new } let(:insert_datetime_modal) { PageObjects::Modals::InsertDateTime.new }
@ -163,6 +164,7 @@ describe "Local dates", type: :system do
topic_page.visit_topic(topic) topic_page.visit_topic(topic)
topic_page.expand_post_actions(topic.first_post) topic_page.expand_post_actions(topic.first_post)
topic_page.click_post_action_button(topic.first_post, :bookmark) topic_page.click_post_action_button(topic.first_post, :bookmark)
bookmark_menu.click_menu_option("custom")
bookmark_modal.select_preset_reminder(:post_local_date) bookmark_modal.select_preset_reminder(:post_local_date)
expect(topic_page).to have_post_bookmarked(topic.first_post) expect(topic_page).to have_post_bookmarked(topic.first_post)
bookmark = Bookmark.find_by(bookmarkable: topic.first_post, user: current_user) bookmark = Bookmark.find_by(bookmarkable: topic.first_post, user: current_user)
@ -177,6 +179,7 @@ describe "Local dates", type: :system do
topic_page.visit_topic(topic) topic_page.visit_topic(topic)
topic_page.expand_post_actions(topic.first_post) topic_page.expand_post_actions(topic.first_post)
topic_page.click_post_action_button(topic.first_post, :bookmark) topic_page.click_post_action_button(topic.first_post, :bookmark)
bookmark_menu.click_menu_option("custom")
expect(bookmark_modal).to be_open expect(bookmark_modal).to be_open
expect(bookmark_modal).to have_no_preset(:post_local_date) expect(bookmark_modal).to have_no_preset(:post_local_date)
end end

View File

@ -9,63 +9,61 @@ describe "Bookmarking posts and topics", type: :system do
let(:timezone) { "Australia/Brisbane" } let(:timezone) { "Australia/Brisbane" }
let(:topic_page) { PageObjects::Pages::Topic.new } let(:topic_page) { PageObjects::Pages::Topic.new }
let(:bookmark_modal) { PageObjects::Modals::Bookmark.new } let(:bookmark_modal) { PageObjects::Modals::Bookmark.new }
let(:bookmark_menu) { PageObjects::Components::BookmarkMenu.new }
before do before do
current_user.user_option.update!(timezone: timezone) current_user.user_option.update!(timezone: timezone)
sign_in(current_user) sign_in(current_user)
end end
def visit_topic_and_open_bookmark_modal(post) def visit_topic_and_open_bookmark_menu(post, expand_actions: true)
topic_page.visit_topic(topic) topic_page.visit_topic(topic)
topic_page.expand_post_actions(post)
topic_page.expand_post_actions(post) if expand_actions
topic_page.click_post_action_button(post, :bookmark) topic_page.click_post_action_button(post, :bookmark)
end end
it "allows the user to create bookmarks with and without reminders" do it "creates a bookmark on the post as soon as the bookmark button is clicked" do
visit_topic_and_open_bookmark_modal(post) visit_topic_and_open_bookmark_menu(post)
bookmark_modal.fill_name("something important")
bookmark_modal.save
expect(bookmark_menu).to be_open
expect(page).to have_content(I18n.t("js.bookmarks.bookmarked_success"))
expect(topic_page).to have_post_bookmarked(post) expect(topic_page).to have_post_bookmarked(post)
bookmark = Bookmark.find_by(bookmarkable: post, user: current_user) expect(Bookmark.find_by(bookmarkable: post, user: current_user)).to be_truthy
expect(bookmark.name).to eq("something important") end
expect(bookmark.reminder_at).to eq(nil)
visit_topic_and_open_bookmark_modal(post_2) it "updates the created bookmark with a selected reminder option from the bookmark menu" do
visit_topic_and_open_bookmark_menu(post)
expect(bookmark_menu).to be_open
expect(page).to have_content(I18n.t("js.bookmarks.bookmarked_success"))
bookmark_menu.click_menu_option("tomorrow")
expect(page).to have_content(I18n.t("js.bookmarks.reminder_set_success"))
expect(Bookmark.find_by(bookmarkable: post, user: current_user).reminder_at).not_to be_blank
end
it "can set a reminder from the bookmark modal using the custom bookmark menu option" do
visit_topic_and_open_bookmark_menu(post)
bookmark_menu.click_menu_option("custom")
bookmark_modal.select_preset_reminder(:tomorrow) bookmark_modal.select_preset_reminder(:tomorrow)
expect(topic_page).to have_post_bookmarked(post_2)
bookmark = Bookmark.find_by(bookmarkable: post_2, user: current_user)
expect(bookmark.reminder_at).not_to eq(nil)
expect(bookmark.reminder_set_at).not_to eq(nil)
end
it "does not create a bookmark if the modal is closed with the cancel button" do
visit_topic_and_open_bookmark_modal(post)
bookmark_modal.fill_name("something important")
bookmark_modal.cancel
expect(topic_page).to have_no_post_bookmarked(post)
expect(Bookmark.exists?(bookmarkable: post, user: current_user)).to eq(false)
end
it "creates a bookmark if the modal is closed by clicking outside the modal window" do
visit_topic_and_open_bookmark_modal(post)
bookmark_modal.fill_name("something important")
bookmark_modal.click_outside
expect(topic_page).to have_post_bookmarked(post) expect(topic_page).to have_post_bookmarked(post)
expect(Bookmark.find_by(bookmarkable: post, user: current_user).reminder_at).not_to be_blank
end end
it "allows choosing a different auto_delete_preference to the user preference and remembers it when reopening the modal" do it "allows choosing a different auto_delete_preference to the user preference and remembers it when reopening the modal" do
current_user.user_option.update!( current_user.user_option.update!(
bookmark_auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply], bookmark_auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply],
) )
visit_topic_and_open_bookmark_modal(post_2) visit_topic_and_open_bookmark_menu(post_2)
bookmark_menu.click_menu_option("custom")
expect(bookmark_modal).to be_open
# TODO (martin) Not sure why, but I need to click this twice for the panel to open :/
bookmark_modal.open_options_panel bookmark_modal.open_options_panel
bookmark_modal.open_options_panel
expect(bookmark_modal).to have_auto_delete_preference( expect(bookmark_modal).to have_auto_delete_preference(
Bookmark.auto_delete_preferences[:on_owner_reply], Bookmark.auto_delete_preferences[:on_owner_reply],
) )
@ -73,6 +71,7 @@ describe "Bookmarking posts and topics", type: :system do
bookmark_modal.save bookmark_modal.save
expect(topic_page).to have_post_bookmarked(post_2) expect(topic_page).to have_post_bookmarked(post_2)
topic_page.click_post_action_button(post_2, :bookmark) topic_page.click_post_action_button(post_2, :bookmark)
bookmark_menu.click_menu_option("edit")
expect(bookmark_modal).to have_open_options_panel expect(bookmark_modal).to have_open_options_panel
expect(bookmark_modal).to have_auto_delete_preference( expect(bookmark_modal).to have_auto_delete_preference(
Bookmark.auto_delete_preferences[:clear_reminder], Bookmark.auto_delete_preferences[:clear_reminder],
@ -125,8 +124,8 @@ describe "Bookmarking posts and topics", type: :system do
end end
it "prefills the name of the bookmark and the custom reminder date and time" do it "prefills the name of the bookmark and the custom reminder date and time" do
topic_page.visit_topic(topic) visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
topic_page.click_post_action_button(post_2, :bookmark) bookmark_menu.click_menu_option("edit")
expect(bookmark_modal).to have_open_options_panel expect(bookmark_modal).to have_open_options_panel
expect(bookmark_modal.name.value).to eq("test name") expect(bookmark_modal.name.value).to eq("test name")
expect(bookmark_modal.existing_reminder_alert).to have_content( expect(bookmark_modal.existing_reminder_alert).to have_content(
@ -142,20 +141,27 @@ describe "Bookmarking posts and topics", type: :system do
end end
it "can delete the bookmark" do it "can delete the bookmark" do
topic_page.visit_topic(topic) visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
topic_page.click_post_action_button(post_2, :bookmark) bookmark_menu.click_menu_option("edit")
bookmark_modal.delete bookmark_modal.delete
bookmark_modal.confirm_delete bookmark_modal.confirm_delete
expect(topic_page).to have_no_post_bookmarked(post_2) expect(topic_page).to have_no_post_bookmarked(post_2)
end end
it "can delete the bookmark from within the menu" do
visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
bookmark_menu.click_menu_option("delete")
expect(topic_page).to have_no_post_bookmarked(post_2)
end
it "does not save edits when pressing cancel" do it "does not save edits when pressing cancel" do
topic_page.visit_topic(topic) visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
topic_page.click_post_action_button(post_2, :bookmark) bookmark_menu.click_menu_option("edit")
bookmark_modal.fill_name("something important") bookmark_modal.fill_name("something important")
bookmark_modal.cancel bookmark_modal.cancel
topic_page.click_post_action_button(post_2, :bookmark) topic_page.click_post_action_button(post_2, :bookmark)
expect(bookmark_modal.name.value).to eq("test name") bookmark_menu.click_menu_option("edit")
expect(bookmark_modal.name.value).to eq("something important")
expect(bookmark.reload.name).to eq("test name") expect(bookmark.reload.name).to eq("test name")
end end
end end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module PageObjects
module Components
class BookmarkMenu < PageObjects::Components::Base
def click_menu_option(option_id)
find(".bookmark-menu__row[data-menu-option-id='#{option_id}']").click
end
def open?
has_css?(".bookmark-menu__body")
end
end
end
end

View File

@ -83,7 +83,7 @@ module PageObjects
def click_post_action_button(post, button) def click_post_action_button(post, button)
case button case button
when :bookmark when :bookmark
post_by_number(post).find(".bookmark.with-reminder").click post_by_number(post).find(".bookmark").click
when :reply when :reply
post_by_number(post).find(".post-controls .reply").click post_by_number(post).find(".post-controls .reply").click
when :flag when :flag
@ -240,10 +240,7 @@ module PageObjects
def is_post_bookmarked(post, bookmarked:) def is_post_bookmarked(post, bookmarked:)
within post_by_number(post) do within post_by_number(post) do
page.public_send( page.public_send(bookmarked ? :has_css? : :has_no_css?, ".bookmark.bookmarked")
bookmarked ? :has_css? : :has_no_css?,
".bookmark.with-reminder.bookmarked",
)
end end
end end
end end