From 67a8080e338aa62afbd02efc98178b4a67751775 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Fri, 5 Apr 2024 09:25:30 +1000 Subject: [PATCH] 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. --- .../discourse/app/components/bookmark-list.js | 2 +- .../app/components/bookmark-menu.gjs | 250 ++++++++++++++++++ .../app/components/modal/bookmark.hbs | 2 +- .../app/components/modal/bookmark.js | 26 +- .../discourse/app/controllers/topic.js | 8 +- .../instance-initializers/bookmark-menu.js | 28 ++ .../discourse/app/lib/bookmark-form-data.js | 56 ++++ .../javascripts/discourse/app/lib/bookmark.js | 43 --- .../app/lib/post-bookmark-manager.js | 102 +++++++ .../discourse/app/lib/time-shortcut.js | 10 + .../discourse/app/lib/time-utils.js | 4 + .../discourse/app/services/bookmark-api.js | 41 +++ .../discourse/app/templates/topic.hbs | 2 +- .../discourse/app/widgets/bookmark-menu.js | 8 + .../discourse/app/widgets/post-menu.js | 4 +- .../components/float-kit/d-menu-test.js | 37 +++ .../components/float-kit/d-tooltip-test.js | 44 ++- .../float-kit/addon/components/d-menu.gjs | 29 +- .../float-kit/addon/components/d-tooltip.gjs | 20 +- .../float-kit/addon/lib/constants.js | 6 + .../float-kit/addon/lib/float-kit-instance.js | 10 +- .../stylesheets/common/components/_index.scss | 1 + .../common/components/bookmark-menu.scss | 91 +++++++ app/serializers/current_user_serializer.rb | 1 - config/locales/client.en.yml | 12 +- config/locales/server.en.yml | 2 +- .../discourse/lib/chat-message-interactor.js | 8 +- .../spec/system/local_dates_spec.rb | 3 + spec/system/bookmarks_spec.rb | 88 +++--- .../page_objects/components/bookmark_menu.rb | 15 ++ spec/system/page_objects/pages/topic.rb | 7 +- 31 files changed, 825 insertions(+), 135 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/bookmark-menu.gjs create mode 100644 app/assets/javascripts/discourse/app/instance-initializers/bookmark-menu.js create mode 100644 app/assets/javascripts/discourse/app/lib/bookmark-form-data.js create mode 100644 app/assets/javascripts/discourse/app/lib/post-bookmark-manager.js create mode 100644 app/assets/javascripts/discourse/app/services/bookmark-api.js create mode 100644 app/assets/javascripts/discourse/app/widgets/bookmark-menu.js create mode 100644 app/assets/stylesheets/common/components/bookmark-menu.scss create mode 100644 spec/system/page_objects/components/bookmark_menu.rb diff --git a/app/assets/javascripts/discourse/app/components/bookmark-list.js b/app/assets/javascripts/discourse/app/components/bookmark-list.js index 82fd27f856e..9518a62b691 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark-list.js +++ b/app/assets/javascripts/discourse/app/components/bookmark-list.js @@ -4,7 +4,7 @@ import { service } from "@ember/service"; import { Promise } from "rsvp"; import BookmarkModal from "discourse/components/modal/bookmark"; import { ajax } from "discourse/lib/ajax"; -import { BookmarkFormData } from "discourse/lib/bookmark"; +import { BookmarkFormData } from "discourse/lib/bookmark-form-data"; import { openLinkInNewTab, shouldOpenInNewTab, diff --git a/app/assets/javascripts/discourse/app/components/bookmark-menu.gjs b/app/assets/javascripts/discourse/app/components/bookmark-menu.gjs new file mode 100644 index 00000000000..6dcc129aa8d --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bookmark-menu.gjs @@ -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); + } + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/modal/bookmark.hbs b/app/assets/javascripts/discourse/app/components/modal/bookmark.hbs index f0622164fb5..f0b499eefdc 100644 --- a/app/assets/javascripts/discourse/app/components/modal/bookmark.hbs +++ b/app/assets/javascripts/discourse/app/components/modal/bookmark.hbs @@ -4,7 +4,7 @@ @flash={{this.flash}} @flashType="error" id="bookmark-reminder-modal" - class="bookmark-reminder-modal bookmark-with-reminder" + class="bookmark-reminder-modal" data-bookmark-id={{this.bookmark.id}} {{did-insert this.didInsert}} > diff --git a/app/assets/javascripts/discourse/app/components/modal/bookmark.js b/app/assets/javascripts/discourse/app/components/modal/bookmark.js index acb4fe301b0..f0aba9ee0c4 100644 --- a/app/assets/javascripts/discourse/app/components/modal/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/modal/bookmark.js @@ -6,7 +6,6 @@ import { service } from "@ember/service"; import ItsATrap from "@discourse/itsatrap"; import { Promise } from "rsvp"; 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 { formattedReminderTime } from "discourse/lib/bookmark"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; @@ -29,6 +28,7 @@ export default class BookmarkModal extends Component { @service dialog; @service currentUser; @service site; + @service bookmarkApi; @tracked postDetectedLocalDate = null; @tracked postDetectedLocalTime = null; @@ -264,31 +264,19 @@ export default class BookmarkModal extends Component { } if (this.editingExistingBookmark) { - return ajax(`/bookmarks/${this.bookmark.id}`, { - type: "PUT", - data: this.bookmark.saveData, - }).then(() => { - this.args.model.afterSave?.(this.bookmark.saveData); + return this.bookmarkApi.update(this.bookmark).then(() => { + this.args.model.afterSave?.(this.bookmark); }); } else { - return ajax("/bookmarks", { - type: "POST", - data: this.bookmark.saveData, - }).then((response) => { - this.bookmark.id = response.id; - this.args.model.afterSave?.(this.bookmark.saveData); + return this.bookmarkApi.create(this.bookmark).then(() => { + this.args.model.afterSave?.(this.bookmark); }); } } #deleteBookmark() { - return ajax("/bookmarks/" + this.bookmark.id, { - type: "DELETE", - }).then((response) => { - this.args.model.afterDelete?.( - response.topic_bookmarked, - this.bookmark.id - ); + return this.bookmarkApi.delete(this.bookmark.id).then((response) => { + this.args.model.afterDelete?.(response, this.bookmark.id); }); } diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index f2f6bc08349..db720e7164b 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -17,7 +17,7 @@ import JumpToPost from "discourse/components/modal/jump-to-post"; import { spinnerHTML } from "discourse/helpers/loading-spinner"; import { ajax } from "discourse/lib/ajax"; 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 { buildQuote } from "discourse/lib/quote"; import QuoteState from "discourse/lib/quote-state"; @@ -1281,14 +1281,14 @@ export default Controller.extend(bufferedProperty("model"), { this.modal.show(BookmarkModal, { model: { bookmark: new BookmarkFormData(bookmark), - afterSave: (savedData) => { - this._syncBookmarks(savedData); + afterSave: (bookmarkFormData) => { + this._syncBookmarks(bookmarkFormData.saveData); this.model.set("bookmarking", false); this.model.set("bookmarked", true); this.model.incrementProperty("bookmarksWereChanged"); this.appEvents.trigger( "bookmarks:changed", - savedData, + bookmarkFormData.saveData, bookmark.attachedTo() ); }, diff --git a/app/assets/javascripts/discourse/app/instance-initializers/bookmark-menu.js b/app/assets/javascripts/discourse/app/instance-initializers/bookmark-menu.js new file mode 100644 index 00000000000..30d4b28c9cf --- /dev/null +++ b/app/assets/javascripts/discourse/app/instance-initializers/bookmark-menu.js @@ -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() + ), + }; + }, + }); + } + }); + }, +}; diff --git a/app/assets/javascripts/discourse/app/lib/bookmark-form-data.js b/app/assets/javascripts/discourse/app/lib/bookmark-form-data.js new file mode 100644 index 00000000000..0a5d61b82b4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/bookmark-form-data.js @@ -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); + } +} diff --git a/app/assets/javascripts/discourse/app/lib/bookmark.js b/app/assets/javascripts/discourse/app/lib/bookmark.js index 3cb96c43149..c8f3d7283f4 100644 --- a/app/assets/javascripts/discourse/app/lib/bookmark.js +++ b/app/assets/javascripts/discourse/app/lib/bookmark.js @@ -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"; export function formattedReminderTime(reminderAt, timezone) { @@ -20,43 +17,3 @@ export function formattedReminderTime(reminderAt, timezone) { 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, - }; - } -} diff --git a/app/assets/javascripts/discourse/app/lib/post-bookmark-manager.js b/app/assets/javascripts/discourse/app/lib/post-bookmark-manager.js new file mode 100644 index 00000000000..3491a964f66 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/post-bookmark-manager.js @@ -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; + } + } +} diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js index 16c9ee6cb85..6922e47a5dd 100644 --- a/app/assets/javascripts/discourse/app/lib/time-shortcut.js +++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js @@ -1,5 +1,6 @@ import { fourMonths, + inNDays, LATER_TODAY_CUTOFF_HOUR, laterThisWeek, laterToday, @@ -126,6 +127,15 @@ export function timeShortcuts(timezone) { 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() { return { id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, diff --git a/app/assets/javascripts/discourse/app/lib/time-utils.js b/app/assets/javascripts/discourse/app/lib/time-utils.js index 135e386d277..0836253b818 100644 --- a/app/assets/javascripts/discourse/app/lib/time-utils.js +++ b/app/assets/javascripts/discourse/app/lib/time-utils.js @@ -49,6 +49,10 @@ export function twoDays(timezone) { return startOfDay(now(timezone).add(2, "days")); } +export function inNDays(timezone, num) { + return startOfDay(now(timezone).add(num, "days")); +} + export function laterThisWeek(timezone) { return twoDays(timezone); } diff --git a/app/assets/javascripts/discourse/app/services/bookmark-api.js b/app/assets/javascripts/discourse/app/services/bookmark-api.js new file mode 100644 index 00000000000..afbfb2f4f61 --- /dev/null +++ b/app/assets/javascripts/discourse/app/services/bookmark-api.js @@ -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); + } +} diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs index 196dd042787..c4e16168f34 100644 --- a/app/assets/javascripts/discourse/app/templates/topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/topic.hbs @@ -155,7 +155,7 @@ href {{on "click" this.editTopic}} class="edit-topic" - title={{i18n "edit"}} + title={{i18n "edit_topic"}} >{{d-icon "pencil-alt"}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/widgets/bookmark-menu.js b/app/assets/javascripts/discourse/app/widgets/bookmark-menu.js new file mode 100644 index 00000000000..edff13f595a --- /dev/null +++ b/app/assets/javascripts/discourse/app/widgets/bookmark-menu.js @@ -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`` +); diff --git a/app/assets/javascripts/discourse/app/widgets/post-menu.js b/app/assets/javascripts/discourse/app/widgets/post-menu.js index 031029b0960..352d74c9864 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/post-menu.js @@ -387,7 +387,7 @@ registerButton( return; } - let classNames = ["bookmark", "with-reminder"]; + let classNames = ["bookmark"]; let title = "bookmarks.not_bookmarked"; let titleOptions = { name: "" }; @@ -395,6 +395,8 @@ registerButton( classNames.push("bookmarked"); if (attrs.bookmarkReminderAt) { + classNames.push("with-reminder"); + let formattedReminder = formattedReminderTime( attrs.bookmarkReminderAt, currentUser.user_option.timezone diff --git a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-menu-test.js b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-menu-test.js index 77ae36b49c6..41622a22a0c 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-menu-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-menu-test.js @@ -9,6 +9,7 @@ import { hbs } from "ember-cli-htmlbars"; import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; 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) { setupRenderingTest(hooks); @@ -17,6 +18,10 @@ module("Integration | Component | FloatKit | d-menu", function (hooks) { await triggerEvent(".fk-d-menu__trigger", "click"); } + async function close() { + await triggerEvent(".fk-d-menu__trigger.-expanded", "click"); + } + test("@label", async function (assert) { await render(hbs``); @@ -38,6 +43,38 @@ module("Integration | Component | FloatKit | d-menu", function (hooks) { assert.dom(".fk-d-menu").hasText("content"); }); + test("@onRegisterApi", async function (assert) { + this.api = null; + this.onRegisterApi = (api) => (this.api = api); + + await render( + hbs`` + ); + + assert.ok(this.api instanceof DMenuInstance); + }); + + test("@onShow", async function (assert) { + this.test = false; + this.onShow = () => (this.test = true); + + await render(hbs``); + await open(); + + assert.strictEqual(this.test, true); + }); + + test("@onClose", async function (assert) { + this.test = false; + this.onClose = () => (this.test = true); + + await render(hbs``); + await open(); + await close(); + + assert.strictEqual(this.test, true); + }); + test("-expanded class", async function (assert) { await render(hbs``); diff --git a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-tooltip-test.js b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-tooltip-test.js index b7ff3ebe2b2..a2aeac55722 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-tooltip-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-tooltip-test.js @@ -9,6 +9,7 @@ import { hbs } from "ember-cli-htmlbars"; import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; 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) { setupRenderingTest(hooks); @@ -17,6 +18,10 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) { await triggerEvent(".fk-d-tooltip__trigger", "mousemove"); } + async function close() { + await triggerKeyEvent(document.activeElement, "keydown", "Escape"); + } + test("@label", async function (assert) { await render(hbs``); @@ -38,8 +43,41 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) { assert.dom(".fk-d-tooltip").hasText("content"); }); + test("@onRegisterApi", async function (assert) { + this.api = null; + this.onRegisterApi = (api) => (this.api = api); + + await render( + hbs`` + ); + + assert.ok(this.api instanceof DTooltipInstance); + }); + + test("@onShow", async function (assert) { + this.test = false; + this.onShow = () => (this.test = true); + + await render(hbs``); + + await hover(); + + assert.strictEqual(this.test, true); + }); + + test("@onClose", async function (assert) { + this.test = false; + this.onClose = () => (this.test = true); + + await render(hbs``); + await hover(); + await close(); + + assert.strictEqual(this.test, true); + }); + test("-expanded class", async function (assert) { - await render(hbs``); + await render(hbs``); assert.dom(".fk-d-tooltip__trigger").doesNotHaveClass("-expanded"); @@ -140,7 +178,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) { hbs`` ); await hover(); - await triggerKeyEvent(document.activeElement, "keydown", "Escape"); + await close(); assert.dom(".fk-d-tooltip").doesNotExist(); @@ -148,7 +186,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) { hbs`` ); await hover(); - await triggerKeyEvent(document.activeElement, "keydown", "Escape"); + await close(); assert.dom(".fk-d-tooltip").exists(); }); diff --git a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs index 9eb86d18804..d085aa458fe 100644 --- a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs +++ b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs @@ -1,11 +1,14 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; 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 DButton from "discourse/components/d-button"; import concatClass from "discourse/helpers/concat-class"; 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"; export default class DMenu extends Component { @@ -13,9 +16,9 @@ export default class DMenu extends Component { @tracked menuInstance = null; - registerTrigger = modifier((element) => { + registerTrigger = modifier((element, [properties]) => { const options = { - ...this.args, + ...properties, ...{ autoUpdate: true, listeners: true, @@ -28,6 +31,8 @@ export default class DMenu extends Component { this.menuInstance = instance; + this.options.onRegisterApi?.(this.menuInstance); + return () => { 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; + } +