mirror of
https://github.com/discourse/discourse.git
synced 2025-02-17 09:22:44 +08:00
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:
parent
63594fa643
commit
67a8080e33
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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}}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -155,7 +155,7 @@
|
|||
href
|
||||
{{on "click" this.editTopic}}
|
||||
class="edit-topic"
|
||||
title={{i18n "edit"}}
|
||||
title={{i18n "edit_topic"}}
|
||||
>{{d-icon "pencil-alt"}}</a>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -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}} />`
|
||||
);
|
|
@ -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
|
||||
|
|
|
@ -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`<DMenu @inline={{true}} @label="label" />`);
|
||||
|
||||
|
@ -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`<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) {
|
||||
await render(hbs`<DMenu @inline={{true}} @label="label" />`);
|
||||
|
||||
|
|
|
@ -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`<DTooltip @inline={{true}} @label="label" />`);
|
||||
|
||||
|
@ -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`<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) {
|
||||
await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
|
||||
await render(hbs`<DTooltip @inline={{true}} @label="label" />`);
|
||||
|
||||
assert.dom(".fk-d-tooltip__trigger").doesNotHaveClass("-expanded");
|
||||
|
||||
|
@ -140,7 +178,7 @@ module("Integration | Component | FloatKit | d-tooltip", function (hooks) {
|
|||
hbs`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{true}} />`
|
||||
);
|
||||
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`<DTooltip @inline={{true}} @label="label" @closeOnEscape={{false}} />`
|
||||
);
|
||||
await hover();
|
||||
await triggerKeyEvent(document.activeElement, "keydown", "Escape");
|
||||
await close();
|
||||
|
||||
assert.dom(".fk-d-tooltip").exists();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
<template>
|
||||
<DButton
|
||||
class={{concatClass
|
||||
"fk-d-menu__trigger"
|
||||
(if this.menuInstance.expanded "-expanded")
|
||||
(concat this.options.identifier "-trigger")
|
||||
}}
|
||||
id={{this.menuInstance.id}}
|
||||
data-identifier={{this.options.identifier}}
|
||||
|
@ -67,7 +83,7 @@ export default class DMenu extends Component {
|
|||
@translatedTitle={{@title}}
|
||||
@disabled={{@disabled}}
|
||||
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
||||
{{this.registerTrigger}}
|
||||
{{this.registerTrigger (this.allowedProperties)}}
|
||||
...attributes
|
||||
>
|
||||
{{#if (has-block "trigger")}}
|
||||
|
@ -79,7 +95,10 @@ export default class DMenu extends Component {
|
|||
<DFloatBody
|
||||
@instance={{this.menuInstance}}
|
||||
@trapTab={{this.options.trapTab}}
|
||||
@mainClass="fk-d-menu"
|
||||
@mainClass={{concatClass
|
||||
"fk-d-menu"
|
||||
(concat this.options.identifier "-content")
|
||||
}}
|
||||
@innerClass="fk-d-menu__inner-content"
|
||||
@role="dialog"
|
||||
@inline={{this.options.inline}}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { and } from "truth-helpers";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
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";
|
||||
|
||||
export default class DTooltip extends Component {
|
||||
|
@ -15,9 +17,9 @@ export default class DTooltip extends Component {
|
|||
|
||||
@tracked tooltipInstance = null;
|
||||
|
||||
registerTrigger = modifier((element) => {
|
||||
registerTrigger = modifier((element, [properties]) => {
|
||||
const options = {
|
||||
...this.args,
|
||||
...properties,
|
||||
...{
|
||||
listeners: true,
|
||||
beforeTrigger: (instance) => {
|
||||
|
@ -30,6 +32,8 @@ export default class DTooltip extends Component {
|
|||
|
||||
this.tooltipInstance = instance;
|
||||
|
||||
this.options.onRegisterApi?.(instance);
|
||||
|
||||
return () => {
|
||||
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>
|
||||
<span
|
||||
class={{concatClass
|
||||
|
@ -61,7 +75,7 @@ export default class DTooltip extends Component {
|
|||
data-identifier={{this.options.identifier}}
|
||||
data-trigger
|
||||
aria-expanded={{if this.tooltipInstance.expanded "true" "false"}}
|
||||
{{this.registerTrigger}}
|
||||
{{this.registerTrigger (this.allowedProperties)}}
|
||||
...attributes
|
||||
>
|
||||
<div class="fk-d-tooltip__trigger-container">
|
||||
|
|
|
@ -35,6 +35,9 @@ export const TOOLTIP = {
|
|||
fallbackPlacements: FLOAT_UI_PLACEMENTS,
|
||||
autoUpdate: true,
|
||||
trapTab: true,
|
||||
onClose: null,
|
||||
onShow: null,
|
||||
onRegisterApi: null,
|
||||
},
|
||||
portalOutletId: "d-tooltip-portal-outlet",
|
||||
};
|
||||
|
@ -62,6 +65,9 @@ export const MENU = {
|
|||
autoUpdate: true,
|
||||
trapTab: true,
|
||||
extraClassName: null,
|
||||
onClose: null,
|
||||
onShow: null,
|
||||
onRegisterApi: null,
|
||||
},
|
||||
portalOutletId: "d-menu-portal-outlet",
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { cancel, next } from "@ember/runloop";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
@ -22,11 +22,19 @@ export default class FloatKitInstance {
|
|||
@action
|
||||
show() {
|
||||
this.expanded = true;
|
||||
|
||||
next(() => {
|
||||
this.options.onShow?.();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.expanded = false;
|
||||
|
||||
next(() => {
|
||||
this.options.onClose?.();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@import "banner";
|
||||
@import "bookmark-list";
|
||||
@import "bookmark-modal";
|
||||
@import "bookmark-menu";
|
||||
@import "buttons";
|
||||
@import "color-input";
|
||||
@import "char-counter";
|
||||
|
|
91
app/assets/stylesheets/common/components/bookmark-menu.scss
Normal file
91
app/assets/stylesheets/common/components/bookmark-menu.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -73,7 +73,6 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:sidebar_sections,
|
||||
:new_new_view_enabled?,
|
||||
:use_experimental_topic_bulk_actions?,
|
||||
:use_experimental_topic_bulk_actions?,
|
||||
:use_admin_sidebar,
|
||||
:glimmer_header_enabled?,
|
||||
:can_view_raw_email
|
||||
|
|
|
@ -259,7 +259,8 @@ en:
|
|||
us_west_2: "US West (Oregon)"
|
||||
|
||||
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"
|
||||
not_implemented: "That feature hasn't been implemented yet, sorry!"
|
||||
no_value: "No"
|
||||
|
@ -353,6 +354,10 @@ en:
|
|||
unbookmark_with_reminder: "Click to remove all bookmarks and reminders in this topic"
|
||||
|
||||
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_generic: "You've bookmarked this. %{name}"
|
||||
create: "Create bookmark"
|
||||
|
@ -375,8 +380,11 @@ en:
|
|||
when_reminder_sent: "Delete bookmark"
|
||||
on_owner_reply: "Delete bookmark, once I reply"
|
||||
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: "Search"
|
||||
bookmark: "Bookmark"
|
||||
reminders:
|
||||
today_with_time: "today at %{time}"
|
||||
tomorrow_with_time: "tomorrow at %{time}"
|
||||
|
@ -700,6 +708,7 @@ en:
|
|||
in_two_hours: "In two hours"
|
||||
later_today: "Later today"
|
||||
two_days: "Two days"
|
||||
three_days: "In three days"
|
||||
next_business_day: "Next business day"
|
||||
tomorrow: "Tomorrow"
|
||||
post_local_date: "Date in post"
|
||||
|
@ -721,6 +730,7 @@ en:
|
|||
never: "Never"
|
||||
last_custom: "Last custom datetime"
|
||||
custom: "Custom date and time"
|
||||
custom_short: "Custom..."
|
||||
select_timeframe: "Select a timeframe"
|
||||
|
||||
user_action:
|
||||
|
|
|
@ -2617,8 +2617,8 @@ en:
|
|||
enable_custom_sidebar_sections: "EXPERIMENTAL: Enable custom sidebar sections"
|
||||
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_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_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."
|
||||
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."
|
||||
|
|
|
@ -5,7 +5,7 @@ import { service } from "@ember/service";
|
|||
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||
import FlagModal from "discourse/components/modal/flag";
|
||||
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 Bookmark from "discourse/models/bookmark";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
|
@ -337,12 +337,12 @@ export default class ChatMessageInteractor {
|
|||
this.message.id
|
||||
)
|
||||
),
|
||||
afterSave: (savedData) => {
|
||||
const bookmark = Bookmark.create(savedData);
|
||||
afterSave: (bookmarkFormData) => {
|
||||
const bookmark = Bookmark.create(bookmarkFormData.saveData);
|
||||
this.message.bookmark = bookmark;
|
||||
this.appEvents.trigger(
|
||||
"bookmarks:changed",
|
||||
savedData,
|
||||
bookmarkFormData.saveData,
|
||||
bookmark.attachedTo()
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@ describe "Local dates", type: :system do
|
|||
let(:year) { Time.zone.now.year + 1 }
|
||||
let(:month) { Time.zone.now.month }
|
||||
let(:bookmark_modal) { PageObjects::Modals::Bookmark.new }
|
||||
let(:bookmark_menu) { PageObjects::Components::BookmarkMenu.new }
|
||||
let(:composer) { PageObjects::Components::Composer.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.expand_post_actions(topic.first_post)
|
||||
topic_page.click_post_action_button(topic.first_post, :bookmark)
|
||||
bookmark_menu.click_menu_option("custom")
|
||||
bookmark_modal.select_preset_reminder(:post_local_date)
|
||||
expect(topic_page).to have_post_bookmarked(topic.first_post)
|
||||
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.expand_post_actions(topic.first_post)
|
||||
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 have_no_preset(:post_local_date)
|
||||
end
|
||||
|
|
|
@ -9,63 +9,61 @@ describe "Bookmarking posts and topics", type: :system do
|
|||
let(:timezone) { "Australia/Brisbane" }
|
||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||
let(:bookmark_modal) { PageObjects::Modals::Bookmark.new }
|
||||
let(:bookmark_menu) { PageObjects::Components::BookmarkMenu.new }
|
||||
|
||||
before do
|
||||
current_user.user_option.update!(timezone: timezone)
|
||||
sign_in(current_user)
|
||||
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.expand_post_actions(post)
|
||||
|
||||
topic_page.expand_post_actions(post) if expand_actions
|
||||
|
||||
topic_page.click_post_action_button(post, :bookmark)
|
||||
end
|
||||
|
||||
it "allows the user to create bookmarks with and without reminders" do
|
||||
visit_topic_and_open_bookmark_modal(post)
|
||||
|
||||
bookmark_modal.fill_name("something important")
|
||||
bookmark_modal.save
|
||||
it "creates a bookmark on the post as soon as the bookmark button is clicked" 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"))
|
||||
expect(topic_page).to have_post_bookmarked(post)
|
||||
bookmark = Bookmark.find_by(bookmarkable: post, user: current_user)
|
||||
expect(bookmark.name).to eq("something important")
|
||||
expect(bookmark.reminder_at).to eq(nil)
|
||||
expect(Bookmark.find_by(bookmarkable: post, user: current_user)).to be_truthy
|
||||
end
|
||||
|
||||
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)
|
||||
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(Bookmark.find_by(bookmarkable: post, user: current_user).reminder_at).not_to be_blank
|
||||
end
|
||||
|
||||
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!(
|
||||
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
|
||||
|
||||
expect(bookmark_modal).to have_auto_delete_preference(
|
||||
Bookmark.auto_delete_preferences[:on_owner_reply],
|
||||
)
|
||||
|
@ -73,6 +71,7 @@ describe "Bookmarking posts and topics", type: :system do
|
|||
bookmark_modal.save
|
||||
expect(topic_page).to have_post_bookmarked(post_2)
|
||||
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_auto_delete_preference(
|
||||
Bookmark.auto_delete_preferences[:clear_reminder],
|
||||
|
@ -125,8 +124,8 @@ describe "Bookmarking posts and topics", type: :system do
|
|||
end
|
||||
|
||||
it "prefills the name of the bookmark and the custom reminder date and time" do
|
||||
topic_page.visit_topic(topic)
|
||||
topic_page.click_post_action_button(post_2, :bookmark)
|
||||
visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
|
||||
bookmark_menu.click_menu_option("edit")
|
||||
expect(bookmark_modal).to have_open_options_panel
|
||||
expect(bookmark_modal.name.value).to eq("test name")
|
||||
expect(bookmark_modal.existing_reminder_alert).to have_content(
|
||||
|
@ -142,20 +141,27 @@ describe "Bookmarking posts and topics", type: :system do
|
|||
end
|
||||
|
||||
it "can delete the bookmark" do
|
||||
topic_page.visit_topic(topic)
|
||||
topic_page.click_post_action_button(post_2, :bookmark)
|
||||
visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
|
||||
bookmark_menu.click_menu_option("edit")
|
||||
bookmark_modal.delete
|
||||
bookmark_modal.confirm_delete
|
||||
expect(topic_page).to have_no_post_bookmarked(post_2)
|
||||
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
|
||||
topic_page.visit_topic(topic)
|
||||
topic_page.click_post_action_button(post_2, :bookmark)
|
||||
visit_topic_and_open_bookmark_menu(post_2, expand_actions: false)
|
||||
bookmark_menu.click_menu_option("edit")
|
||||
bookmark_modal.fill_name("something important")
|
||||
bookmark_modal.cancel
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
|
15
spec/system/page_objects/components/bookmark_menu.rb
Normal file
15
spec/system/page_objects/components/bookmark_menu.rb
Normal 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
|
|
@ -83,7 +83,7 @@ module PageObjects
|
|||
def click_post_action_button(post, button)
|
||||
case button
|
||||
when :bookmark
|
||||
post_by_number(post).find(".bookmark.with-reminder").click
|
||||
post_by_number(post).find(".bookmark").click
|
||||
when :reply
|
||||
post_by_number(post).find(".post-controls .reply").click
|
||||
when :flag
|
||||
|
@ -240,10 +240,7 @@ module PageObjects
|
|||
|
||||
def is_post_bookmarked(post, bookmarked:)
|
||||
within post_by_number(post) do
|
||||
page.public_send(
|
||||
bookmarked ? :has_css? : :has_no_css?,
|
||||
".bookmark.with-reminder.bookmarked",
|
||||
)
|
||||
page.public_send(bookmarked ? :has_css? : :has_no_css?, ".bookmark.bookmarked")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user