mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:15:05 +08:00
DEV: Move Bookmark modal/component to use d-modal (#22532)
c.f. https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057 This also converts the Bookmark component to a Glimmer component.
This commit is contained in:
parent
9e8010df8b
commit
6459922993
|
@ -1,6 +1,7 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
|
import { BookmarkFormData } from "discourse/lib/bookmark";
|
||||||
|
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import {
|
import {
|
||||||
openLinkInNewTab,
|
openLinkInNewTab,
|
||||||
|
@ -12,6 +13,7 @@ import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
dialog: service(),
|
dialog: service(),
|
||||||
|
modal: service(),
|
||||||
classNames: ["bookmark-list-wrapper"],
|
classNames: ["bookmark-list-wrapper"],
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -55,8 +57,10 @@ export default Component.extend({
|
||||||
|
|
||||||
@action
|
@action
|
||||||
editBookmark(bookmark) {
|
editBookmark(bookmark) {
|
||||||
openBookmarkModal(bookmark, {
|
this.modal.show(BookmarkModal, {
|
||||||
onAfterSave: (savedData) => {
|
model: {
|
||||||
|
bookmark: new BookmarkFormData(bookmark),
|
||||||
|
afterSave: (savedData) => {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
"bookmarks:changed",
|
"bookmarks:changed",
|
||||||
savedData,
|
savedData,
|
||||||
|
@ -64,9 +68,10 @@ export default Component.extend({
|
||||||
);
|
);
|
||||||
this.reload();
|
this.reload();
|
||||||
},
|
},
|
||||||
onAfterDelete: () => {
|
afterDelete: () => {
|
||||||
this.reload();
|
this.reload();
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
|
||||||
{{#if this.errorMessage}}
|
|
||||||
<div class="control-group">
|
|
||||||
<div class="controls">
|
|
||||||
<div class="alert alert-error">{{this.errorMessage}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="control-group bookmark-name-wrap">
|
|
||||||
<Input
|
|
||||||
id="bookmark-name"
|
|
||||||
@value={{this.model.name}}
|
|
||||||
name="bookmark-name"
|
|
||||||
class="bookmark-name"
|
|
||||||
@enter={{action "saveAndClose"}}
|
|
||||||
placeholder={{i18n "post.bookmarks.name_placeholder"}}
|
|
||||||
maxlength="100"
|
|
||||||
aria-label={{i18n "post.bookmarks.name_input_label"}}
|
|
||||||
/>
|
|
||||||
<DButton
|
|
||||||
@icon="cog"
|
|
||||||
@action={{action "toggleShowOptions"}}
|
|
||||||
@class="bookmark-options-button"
|
|
||||||
@ariaLabel="post.bookmarks.options"
|
|
||||||
@title="post.bookmarks.options"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.showOptions}}
|
|
||||||
<div class="bookmark-options-panel">
|
|
||||||
<label class="control-label" for="bookmark_auto_delete_preference">{{i18n
|
|
||||||
"bookmarks.auto_delete_preference.label"
|
|
||||||
}}</label>
|
|
||||||
<ComboBox
|
|
||||||
@content={{this.autoDeletePreferences}}
|
|
||||||
@value={{this.autoDeletePreference}}
|
|
||||||
@class="bookmark-option-selector"
|
|
||||||
@onChange={{action (mut this.autoDeletePreference)}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showExistingReminderAt}}
|
|
||||||
<div class="alert alert-info existing-reminder-at-alert">
|
|
||||||
{{d-icon "far-clock"}}
|
|
||||||
<span>{{i18n
|
|
||||||
"bookmarks.reminders.existing_reminder"
|
|
||||||
at_date_time=this.existingReminderAtFormatted
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label">
|
|
||||||
{{i18n "post.bookmarks.set_reminder"}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{{#if this.userHasTimezoneSet}}
|
|
||||||
<TimeShortcutPicker
|
|
||||||
@timeShortcuts={{this.timeOptions}}
|
|
||||||
@prefilledDatetime={{this.prefilledDatetime}}
|
|
||||||
@onTimeSelected={{action "onTimeSelected"}}
|
|
||||||
@hiddenOptions={{this.hiddenTimeShortcutOptions}}
|
|
||||||
@customLabels={{this.customTimeShortcutLabels}}
|
|
||||||
@_itsatrap={{this._itsatrap}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<div class="alert alert-info">{{html-safe
|
|
||||||
(i18n "bookmarks.no_timezone" basePath=(base-path))
|
|
||||||
}}</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer control-group">
|
|
||||||
<DButton
|
|
||||||
@id="save-bookmark"
|
|
||||||
@label="bookmarks.save"
|
|
||||||
@class="btn-primary"
|
|
||||||
@action={{action "saveAndClose"}}
|
|
||||||
/>
|
|
||||||
<DModalCancel @close={{action "closeWithoutSavingBookmark"}} />
|
|
||||||
{{#if this.showDelete}}
|
|
||||||
<DButton
|
|
||||||
@id="delete-bookmark"
|
|
||||||
@icon="trash-alt"
|
|
||||||
@class="delete-bookmark btn-danger"
|
|
||||||
@action={{action "delete"}}
|
|
||||||
@ariaLabel="post.bookmarks.actions.delete_bookmark.name"
|
|
||||||
@title="post.bookmarks.actions.delete_bookmark.name"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</ConditionalLoadingSpinner>
|
|
|
@ -1,412 +0,0 @@
|
||||||
import { now, parseCustomDatetime, startOfDay } from "discourse/lib/time-utils";
|
|
||||||
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
|
||||||
import Component from "@ember/component";
|
|
||||||
import I18n from "I18n";
|
|
||||||
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
|
|
||||||
import ItsATrap from "@discourse/itsatrap";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import {
|
|
||||||
TIME_SHORTCUT_TYPES,
|
|
||||||
defaultTimeShortcuts,
|
|
||||||
} from "discourse/lib/time-shortcut";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
|
||||||
import { formattedReminderTime } from "discourse/lib/bookmark";
|
|
||||||
import { and, notEmpty } from "@ember/object/computed";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import discourseLater from "discourse-common/lib/later";
|
|
||||||
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
|
|
||||||
const BOOKMARK_BINDINGS = {
|
|
||||||
enter: { handler: "saveAndClose" },
|
|
||||||
"d d": { handler: "delete" },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
dialog: service(),
|
|
||||||
tagName: "",
|
|
||||||
errorMessage: null,
|
|
||||||
selectedReminderType: null,
|
|
||||||
_closeWithoutSaving: null,
|
|
||||||
_savingBookmarkManually: null,
|
|
||||||
_saving: null,
|
|
||||||
_deleting: null,
|
|
||||||
_itsatrap: null,
|
|
||||||
postDetectedLocalDate: null,
|
|
||||||
postDetectedLocalTime: null,
|
|
||||||
postDetectedLocalTimezone: null,
|
|
||||||
prefilledDatetime: null,
|
|
||||||
userTimezone: null,
|
|
||||||
showOptions: null,
|
|
||||||
model: null,
|
|
||||||
afterSave: null,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
errorMessage: null,
|
|
||||||
selectedReminderType: TIME_SHORTCUT_TYPES.NONE,
|
|
||||||
_closeWithoutSaving: false,
|
|
||||||
_savingBookmarkManually: false,
|
|
||||||
_saving: false,
|
|
||||||
_deleting: false,
|
|
||||||
postDetectedLocalDate: null,
|
|
||||||
postDetectedLocalTime: null,
|
|
||||||
postDetectedLocalTimezone: null,
|
|
||||||
prefilledDatetime: null,
|
|
||||||
userTimezone: this.currentUser.user_option.timezone,
|
|
||||||
showOptions: false,
|
|
||||||
_itsatrap: new ItsATrap(),
|
|
||||||
autoDeletePreference:
|
|
||||||
this.model.autoDeletePreference ??
|
|
||||||
AUTO_DELETE_PREFERENCES.CLEAR_REMINDER,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerOnCloseHandler(this._onModalClose);
|
|
||||||
this._bindKeyboardShortcuts();
|
|
||||||
|
|
||||||
if (this.editingExistingBookmark) {
|
|
||||||
this._initializeExistingBookmarkData();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loadPostLocalDates();
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
discourseLater(() => {
|
|
||||||
if (this.site.isMobileDevice) {
|
|
||||||
document.getElementById("bookmark-name").blur();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// we want to make sure the options panel opens so the user
|
|
||||||
// knows they have set these options previously.
|
|
||||||
if (this.model.id) {
|
|
||||||
this.set("showOptions", true);
|
|
||||||
} else {
|
|
||||||
document.getElementById("tap_tile_none").classList.add("active");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_initializeExistingBookmarkData() {
|
|
||||||
if (this.existingBookmarkHasReminder) {
|
|
||||||
this.set("prefilledDatetime", this.model.reminderAt);
|
|
||||||
|
|
||||||
let parsedDatetime = parseCustomDatetime(
|
|
||||||
this.prefilledDatetime,
|
|
||||||
null,
|
|
||||||
this.userTimezone
|
|
||||||
);
|
|
||||||
|
|
||||||
this.set("selectedDatetime", parsedDatetime);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindKeyboardShortcuts() {
|
|
||||||
KeyboardShortcuts.pause();
|
|
||||||
|
|
||||||
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
|
|
||||||
this._itsatrap.bind(shortcut, () => {
|
|
||||||
let binding = BOOKMARK_BINDINGS[shortcut];
|
|
||||||
this.send(binding.handler);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_loadPostLocalDates() {
|
|
||||||
if (this.model.bookmarkableType !== "Post") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let postEl = document.querySelector(
|
|
||||||
`[data-post-id="${this.model.bookmarkableId}"]`
|
|
||||||
);
|
|
||||||
let localDateEl;
|
|
||||||
if (postEl) {
|
|
||||||
localDateEl = postEl.querySelector(".discourse-local-date");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localDateEl) {
|
|
||||||
this.setProperties({
|
|
||||||
postDetectedLocalDate: localDateEl.dataset.date,
|
|
||||||
postDetectedLocalTime: localDateEl.dataset.time,
|
|
||||||
postDetectedLocalTimezone: localDateEl.dataset.timezone,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_saveBookmark() {
|
|
||||||
let reminderAt;
|
|
||||||
if (this.selectedReminderType) {
|
|
||||||
reminderAt = this.selectedDatetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
|
|
||||||
|
|
||||||
if (this.selectedReminderType === TIME_SHORTCUT_TYPES.CUSTOM) {
|
|
||||||
if (!reminderAt) {
|
|
||||||
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
reminder_at: reminderAtISO,
|
|
||||||
name: this.model.name,
|
|
||||||
id: this.model.id,
|
|
||||||
auto_delete_preference: this.autoDeletePreference,
|
|
||||||
};
|
|
||||||
|
|
||||||
data.bookmarkable_id = this.model.bookmarkableId;
|
|
||||||
data.bookmarkable_type = this.model.bookmarkableType;
|
|
||||||
|
|
||||||
if (this.editingExistingBookmark) {
|
|
||||||
return ajax(`/bookmarks/${this.model.id}`, {
|
|
||||||
type: "PUT",
|
|
||||||
data,
|
|
||||||
}).then((response) => {
|
|
||||||
this._executeAfterSave(response, reminderAtISO);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return ajax("/bookmarks", { type: "POST", data }).then((response) => {
|
|
||||||
this._executeAfterSave(response, reminderAtISO);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_executeAfterSave(response, reminderAtISO) {
|
|
||||||
if (!this.afterSave) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
reminder_at: reminderAtISO,
|
|
||||||
auto_delete_preference: this.autoDeletePreference,
|
|
||||||
id: this.model.id || response.id,
|
|
||||||
name: this.model.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
data.bookmarkable_id = this.model.bookmarkableId;
|
|
||||||
data.bookmarkable_type = this.model.bookmarkableType;
|
|
||||||
|
|
||||||
this.afterSave(data);
|
|
||||||
},
|
|
||||||
|
|
||||||
_deleteBookmark() {
|
|
||||||
return ajax("/bookmarks/" + this.model.id, {
|
|
||||||
type: "DELETE",
|
|
||||||
}).then((response) => {
|
|
||||||
if (this.afterDelete) {
|
|
||||||
this.afterDelete(response.topic_bookmarked, this.model.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_postLocalDate() {
|
|
||||||
let parsedPostLocalDate = parseCustomDatetime(
|
|
||||||
this.postDetectedLocalDate,
|
|
||||||
this.postDetectedLocalTime,
|
|
||||||
this.userTimezone,
|
|
||||||
this.postDetectedLocalTimezone
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.postDetectedLocalTime) {
|
|
||||||
return startOfDay(parsedPostLocalDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedPostLocalDate;
|
|
||||||
},
|
|
||||||
|
|
||||||
_handleSaveError(e) {
|
|
||||||
this._savingBookmarkManually = false;
|
|
||||||
if (typeof e === "string") {
|
|
||||||
this.dialog.alert(e);
|
|
||||||
} else {
|
|
||||||
popupAjaxError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_onModalClose(closeOpts) {
|
|
||||||
// we want to close without saving if the user already saved
|
|
||||||
// manually or deleted the bookmark, as well as when the modal
|
|
||||||
// is just closed with the X button
|
|
||||||
this._closeWithoutSaving =
|
|
||||||
this._closeWithoutSaving ||
|
|
||||||
closeOpts.initiatedByCloseButton ||
|
|
||||||
closeOpts.initiatedByESC;
|
|
||||||
|
|
||||||
if (!this._closeWithoutSaving && !this._savingBookmarkManually) {
|
|
||||||
this._saveBookmark().catch((e) => this._handleSaveError(e));
|
|
||||||
}
|
|
||||||
if (this.onCloseWithoutSaving && this._closeWithoutSaving) {
|
|
||||||
this.onCloseWithoutSaving();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this._itsatrap?.destroy();
|
|
||||||
this.set("_itsatrap", null);
|
|
||||||
KeyboardShortcuts.unpause();
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("model.reminderAt")
|
|
||||||
showExistingReminderAt(reminderAt) {
|
|
||||||
return reminderAt && Date.parse(reminderAt) > new Date().getTime();
|
|
||||||
},
|
|
||||||
|
|
||||||
showDelete: notEmpty("model.id"),
|
|
||||||
userHasTimezoneSet: notEmpty("userTimezone"),
|
|
||||||
editingExistingBookmark: and("model", "model.id"),
|
|
||||||
existingBookmarkHasReminder: and("model", "model.id", "model.reminderAt"),
|
|
||||||
|
|
||||||
@discourseComputed("postDetectedLocalDate", "postDetectedLocalTime")
|
|
||||||
showPostLocalDate(postDetectedLocalDate, postDetectedLocalTime) {
|
|
||||||
if (!postDetectedLocalTime || !postDetectedLocalDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let postLocalDateTime = this._postLocalDate();
|
|
||||||
if (postLocalDateTime < now(this.userTimezone)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed()
|
|
||||||
autoDeletePreferences: () => {
|
|
||||||
return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => {
|
|
||||||
return {
|
|
||||||
id: AUTO_DELETE_PREFERENCES[key],
|
|
||||||
name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("userTimezone")
|
|
||||||
timeOptions(userTimezone) {
|
|
||||||
const options = defaultTimeShortcuts(userTimezone);
|
|
||||||
|
|
||||||
if (this.showPostLocalDate) {
|
|
||||||
options.push({
|
|
||||||
icon: "globe-americas",
|
|
||||||
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
|
|
||||||
label: "time_shortcut.post_local_date",
|
|
||||||
time: this._postLocalDate(),
|
|
||||||
timeFormatKey: "dates.long_no_year",
|
|
||||||
hidden: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("existingBookmarkHasReminder")
|
|
||||||
customTimeShortcutLabels(existingBookmarkHasReminder) {
|
|
||||||
const labels = {};
|
|
||||||
if (existingBookmarkHasReminder) {
|
|
||||||
labels[TIME_SHORTCUT_TYPES.NONE] =
|
|
||||||
"bookmarks.remove_reminder_keep_bookmark";
|
|
||||||
}
|
|
||||||
return labels;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("editingExistingBookmark", "existingBookmarkHasReminder")
|
|
||||||
hiddenTimeShortcutOptions(
|
|
||||||
editingExistingBookmark,
|
|
||||||
existingBookmarkHasReminder
|
|
||||||
) {
|
|
||||||
if (editingExistingBookmark && !existingBookmarkHasReminder) {
|
|
||||||
return [TIME_SHORTCUT_TYPES.NONE];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("model.reminderAt")
|
|
||||||
existingReminderAtFormatted(existingReminderAt) {
|
|
||||||
return formattedReminderTime(existingReminderAt, this.userTimezone);
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
saveAndClose() {
|
|
||||||
if (this._saving || this._deleting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._saving = true;
|
|
||||||
this._savingBookmarkManually = true;
|
|
||||||
return this._saveBookmark()
|
|
||||||
.then(() => this.closeModal())
|
|
||||||
.catch((e) => this._handleSaveError(e))
|
|
||||||
.finally(() => (this._saving = false));
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleShowOptions() {
|
|
||||||
this.toggleProperty("showOptions");
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
delete() {
|
|
||||||
if (!this.model.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._deleting = true;
|
|
||||||
let deleteAction = () => {
|
|
||||||
this._closeWithoutSaving = true;
|
|
||||||
this._deleteBookmark()
|
|
||||||
.then(() => {
|
|
||||||
this._deleting = false;
|
|
||||||
this.closeModal();
|
|
||||||
})
|
|
||||||
.catch((e) => this._handleSaveError(e));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.existingBookmarkHasReminder) {
|
|
||||||
this.dialog.deleteConfirm({
|
|
||||||
message: I18n.t("bookmarks.confirm_delete"),
|
|
||||||
didConfirm: () => deleteAction(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deleteAction();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
closeWithoutSavingBookmark() {
|
|
||||||
this._closeWithoutSaving = true;
|
|
||||||
this.closeModal();
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
onTimeSelected(type, time) {
|
|
||||||
this.setProperties({ selectedReminderType: type, selectedDatetime: time });
|
|
||||||
|
|
||||||
// if the type is custom, we need to wait for the user to click save, as
|
|
||||||
// they could still be adjusting the date and time
|
|
||||||
if (
|
|
||||||
![TIME_SHORTCUT_TYPES.CUSTOM, TIME_SHORTCUT_TYPES.RELATIVE].includes(type)
|
|
||||||
) {
|
|
||||||
return this.saveAndClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectPostLocalDate(date) {
|
|
||||||
this.setProperties({
|
|
||||||
selectedReminderType: this.reminderTypes.POST_LOCAL_DATE,
|
|
||||||
postLocalDate: date,
|
|
||||||
});
|
|
||||||
return this.saveAndClose();
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<DModal
|
||||||
|
@closeModal={{this.closingModal}}
|
||||||
|
@title={{this.modalTitle}}
|
||||||
|
@flash={{this.flash}}
|
||||||
|
@flashType="error"
|
||||||
|
id="bookmark-reminder-modal"
|
||||||
|
class="bookmark-reminder-modal bookmark-with-reminder"
|
||||||
|
data-bookmark-id={{this.bookmark.id}}
|
||||||
|
{{did-insert this.didInsert}}
|
||||||
|
>
|
||||||
|
<:body>
|
||||||
|
<div class="control-group bookmark-name-wrap">
|
||||||
|
<Input
|
||||||
|
id="bookmark-name"
|
||||||
|
@value={{this.bookmark.name}}
|
||||||
|
name="bookmark-name"
|
||||||
|
class="bookmark-name"
|
||||||
|
@enter={{action "saveAndClose"}}
|
||||||
|
placeholder={{i18n "post.bookmarks.name_placeholder"}}
|
||||||
|
aria-label={{i18n "post.bookmarks.name_input_label"}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@icon="cog"
|
||||||
|
@action={{this.toggleShowOptions}}
|
||||||
|
class="bookmark-options-button"
|
||||||
|
@ariaLabel="post.bookmarks.options"
|
||||||
|
@title="post.bookmarks.options"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.showOptions}}
|
||||||
|
<div class="bookmark-options-panel">
|
||||||
|
<label
|
||||||
|
class="control-label"
|
||||||
|
for="bookmark_auto_delete_preference"
|
||||||
|
>{{i18n "bookmarks.auto_delete_preference.label"}}</label>
|
||||||
|
<ComboBox
|
||||||
|
@content={{this.autoDeletePreferences}}
|
||||||
|
@value={{this.bookmark.autoDeletePreference}}
|
||||||
|
@class="bookmark-option-selector"
|
||||||
|
@id="bookmark-auto-delete-preference"
|
||||||
|
@onChange={{action (mut this.bookmark.autoDeletePreference)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showExistingReminderAt}}
|
||||||
|
<div class="alert alert-info existing-reminder-at-alert">
|
||||||
|
{{d-icon "far-clock"}}
|
||||||
|
<span>{{i18n
|
||||||
|
"bookmarks.reminders.existing_reminder"
|
||||||
|
at_date_time=this.existingReminderAtFormatted
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">
|
||||||
|
{{i18n "post.bookmarks.set_reminder"}}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{{#if this.userHasTimezoneSet}}
|
||||||
|
<TimeShortcutPicker
|
||||||
|
@timeShortcuts={{this.timeOptions}}
|
||||||
|
@prefilledDatetime={{this.prefilledDatetime}}
|
||||||
|
@onTimeSelected={{action "onTimeSelected"}}
|
||||||
|
@hiddenOptions={{this.hiddenTimeShortcutOptions}}
|
||||||
|
@customLabels={{this.customTimeShortcutLabels}}
|
||||||
|
@_itsatrap={{this._itsatrap}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<div class="alert alert-info">{{html-safe
|
||||||
|
(i18n "bookmarks.no_timezone" basePath=(base-path))
|
||||||
|
}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</:body>
|
||||||
|
|
||||||
|
<:footer>
|
||||||
|
<div class="control-group">
|
||||||
|
<DButton
|
||||||
|
id="save-bookmark"
|
||||||
|
@label="bookmarks.save"
|
||||||
|
class="btn-primary"
|
||||||
|
@action={{this.saveAndClose}}
|
||||||
|
/>
|
||||||
|
<DModalCancel @close={{action "closeWithoutSavingBookmark"}} />
|
||||||
|
{{#if this.showDelete}}
|
||||||
|
<DButton
|
||||||
|
id="delete-bookmark"
|
||||||
|
@icon="trash-alt"
|
||||||
|
class="delete-bookmark btn-danger"
|
||||||
|
@action={{this.delete}}
|
||||||
|
@ariaLabel="post.bookmarks.actions.delete_bookmark.name"
|
||||||
|
@title="post.bookmarks.actions.delete_bookmark.name"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</:footer>
|
||||||
|
</DModal>
|
|
@ -0,0 +1,351 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { extractError } from "discourse/lib/ajax-error";
|
||||||
|
import { sanitize } from "discourse/lib/text";
|
||||||
|
import { CLOSE_INITIATED_BY_CLICK_OUTSIDE } from "discourse/components/d-modal";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { now, parseCustomDatetime, startOfDay } from "discourse/lib/time-utils";
|
||||||
|
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
|
||||||
|
import ItsATrap from "@discourse/itsatrap";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
import {
|
||||||
|
TIME_SHORTCUT_TYPES,
|
||||||
|
defaultTimeShortcuts,
|
||||||
|
} from "discourse/lib/time-shortcut";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { formattedReminderTime } from "discourse/lib/bookmark";
|
||||||
|
import { and, notEmpty } from "@ember/object/computed";
|
||||||
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
|
||||||
|
const BOOKMARK_BINDINGS = {
|
||||||
|
enter: { handler: "saveAndClose" },
|
||||||
|
"d d": { handler: "delete" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class BookmarkModal extends Component {
|
||||||
|
@service dialog;
|
||||||
|
@service currentUser;
|
||||||
|
@service site;
|
||||||
|
|
||||||
|
@tracked postDetectedLocalDate = null;
|
||||||
|
@tracked postDetectedLocalTime = null;
|
||||||
|
@tracked postDetectedLocalTimezone = null;
|
||||||
|
@tracked prefilledDatetime = null;
|
||||||
|
@tracked flash = null;
|
||||||
|
@tracked userTimezone = this.currentUser.user_option.timezone;
|
||||||
|
@tracked showOptions = this.args.model.bookmark.id ? true : false;
|
||||||
|
|
||||||
|
@notEmpty("userTimezone")
|
||||||
|
userHasTimezoneSet;
|
||||||
|
|
||||||
|
@notEmpty("bookmark.id")
|
||||||
|
showDelete;
|
||||||
|
|
||||||
|
@notEmpty("bookmark.id")
|
||||||
|
editingExistingBookmark;
|
||||||
|
|
||||||
|
@and("bookmark.id", "bookmark.reminderAt")
|
||||||
|
existingBookmarkHasReminder;
|
||||||
|
|
||||||
|
@tracked _closeWithoutSaving = false;
|
||||||
|
@tracked _savingBookmarkManually = false;
|
||||||
|
@tracked _saving = false;
|
||||||
|
@tracked _deleting = false;
|
||||||
|
|
||||||
|
_itsatrap = new ItsATrap();
|
||||||
|
|
||||||
|
get bookmark() {
|
||||||
|
return this.args.model.bookmark;
|
||||||
|
}
|
||||||
|
|
||||||
|
get modalTitle() {
|
||||||
|
return I18n.t(this.bookmark.id ? "bookmarks.edit" : "bookmarks.create");
|
||||||
|
}
|
||||||
|
|
||||||
|
get autoDeletePreferences() {
|
||||||
|
return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => {
|
||||||
|
return {
|
||||||
|
id: AUTO_DELETE_PREFERENCES[key],
|
||||||
|
name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get showExistingReminderAt() {
|
||||||
|
return (
|
||||||
|
this.bookmark.reminderAt &&
|
||||||
|
Date.parse(this.bookmark.reminderAt) > new Date().getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get existingReminderAtFormatted() {
|
||||||
|
return formattedReminderTime(this.bookmark.reminderAt, this.userTimezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeOptions() {
|
||||||
|
const options = defaultTimeShortcuts(this.userTimezone);
|
||||||
|
|
||||||
|
if (this.showPostLocalDate) {
|
||||||
|
options.push({
|
||||||
|
icon: "globe-americas",
|
||||||
|
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
|
||||||
|
label: "time_shortcut.post_local_date",
|
||||||
|
time: this.#parsedPostLocalDateTime(),
|
||||||
|
timeFormatKey: "dates.long_no_year",
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showPostLocalDate() {
|
||||||
|
if (!this.postDetectedLocalTime || !this.postDetectedLocalDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#parsedPostLocalDateTime() < now(this.userTimezone)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hiddenTimeShortcutOptions() {
|
||||||
|
if (this.editingExistingBookmark && !this.existingBookmarkHasReminder) {
|
||||||
|
return [TIME_SHORTCUT_TYPES.NONE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get customTimeShortcutLabels() {
|
||||||
|
const labels = {};
|
||||||
|
if (this.existingBookmarkHasReminder) {
|
||||||
|
labels[TIME_SHORTCUT_TYPES.NONE] =
|
||||||
|
"bookmarks.remove_reminder_keep_bookmark";
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this._itsatrap?.destroy();
|
||||||
|
this._itsatrap = null;
|
||||||
|
KeyboardShortcuts.unpause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
didInsert() {
|
||||||
|
discourseLater(() => {
|
||||||
|
if (this.site.isMobileDevice) {
|
||||||
|
document.getElementById("bookmark-name").blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.args.model.bookmark.id) {
|
||||||
|
document.getElementById("tap_tile_none").classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#bindKeyboardShortcuts();
|
||||||
|
this.#initializeExistingBookmarkData();
|
||||||
|
this.#loadPostLocalDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
saveAndClose() {
|
||||||
|
this.flash = null;
|
||||||
|
if (this._saving || this._deleting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._saving = true;
|
||||||
|
this._savingBookmarkManually = true;
|
||||||
|
return this.#saveBookmark()
|
||||||
|
.then(() => this.args.closeModal())
|
||||||
|
.catch((error) => this.#handleSaveError(error))
|
||||||
|
.finally(() => {
|
||||||
|
this._saving = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleShowOptions() {
|
||||||
|
this.showOptions = !this.showOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onTimeSelected(type, time) {
|
||||||
|
this.bookmark.selectedReminderType = type;
|
||||||
|
this.bookmark.selectedDatetime = time;
|
||||||
|
|
||||||
|
// If the type is custom, we need to wait for the user to click save, as
|
||||||
|
// they could still be adjusting the date and time
|
||||||
|
if (
|
||||||
|
![TIME_SHORTCUT_TYPES.CUSTOM, TIME_SHORTCUT_TYPES.RELATIVE].includes(type)
|
||||||
|
) {
|
||||||
|
return this.saveAndClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
closingModal(closeModalArgs) {
|
||||||
|
// If the user clicks outside the modal we save automatically for them,
|
||||||
|
// as long as they are not already saving manually or deleting the bookmark.
|
||||||
|
if (
|
||||||
|
closeModalArgs.initiatedBy === CLOSE_INITIATED_BY_CLICK_OUTSIDE &&
|
||||||
|
!this._closeWithoutSaving &&
|
||||||
|
!this._savingBookmarkManually
|
||||||
|
) {
|
||||||
|
this.#saveBookmark()
|
||||||
|
.catch((e) => this.#handleSaveError(e))
|
||||||
|
.then(() => {
|
||||||
|
this.args.closeModal(closeModalArgs);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.args.closeModal(closeModalArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
closeWithoutSavingBookmark() {
|
||||||
|
this._closeWithoutSaving = true;
|
||||||
|
this.args.closeModal({ closeWithoutSaving: this._closeWithoutSaving });
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
delete() {
|
||||||
|
if (!this.bookmark.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deleting = true;
|
||||||
|
const deleteAction = () => {
|
||||||
|
this._closeWithoutSaving = true;
|
||||||
|
this.#deleteBookmark()
|
||||||
|
.then(() => {
|
||||||
|
this._deleting = false;
|
||||||
|
this.args.closeModal({
|
||||||
|
closeWithoutSaving: this._closeWithoutSaving,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => this.#handleSaveError(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.existingBookmarkHasReminder) {
|
||||||
|
this.dialog.deleteConfirm({
|
||||||
|
message: I18n.t("bookmarks.confirm_delete"),
|
||||||
|
didConfirm: () => deleteAction(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deleteAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#parsedPostLocalDateTime() {
|
||||||
|
let parsedPostLocalDate = parseCustomDatetime(
|
||||||
|
this.postDetectedLocalDate,
|
||||||
|
this.postDetectedLocalTime,
|
||||||
|
this.userTimezone,
|
||||||
|
this.postDetectedLocalTimezone
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.postDetectedLocalTime) {
|
||||||
|
return startOfDay(parsedPostLocalDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPostLocalDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
#saveBookmark() {
|
||||||
|
if (this.bookmark.selectedReminderType === TIME_SHORTCUT_TYPES.CUSTOM) {
|
||||||
|
if (!this.bookmark.reminderAtISO) {
|
||||||
|
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingExistingBookmark) {
|
||||||
|
return ajax(`/bookmarks/${this.bookmark.id}`, {
|
||||||
|
type: "PUT",
|
||||||
|
data: this.bookmark.saveData,
|
||||||
|
}).then(() => {
|
||||||
|
this.args.model.afterSave?.(this.bookmark.saveData);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return ajax("/bookmarks", {
|
||||||
|
type: "POST",
|
||||||
|
data: this.bookmark.saveData,
|
||||||
|
}).then((response) => {
|
||||||
|
this.bookmark.id = response.id;
|
||||||
|
this.args.model.afterSave?.(this.bookmark.saveData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#deleteBookmark() {
|
||||||
|
return ajax("/bookmarks/" + this.bookmark.id, {
|
||||||
|
type: "DELETE",
|
||||||
|
}).then((response) => {
|
||||||
|
this.args.model.afterDelete?.(
|
||||||
|
response.topic_bookmarked,
|
||||||
|
this.bookmark.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleSaveError(error) {
|
||||||
|
this._savingBookmarkManually = false;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
this.flash = sanitize(error);
|
||||||
|
} else {
|
||||||
|
this.flash = sanitize(extractError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindKeyboardShortcuts() {
|
||||||
|
KeyboardShortcuts.pause();
|
||||||
|
|
||||||
|
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
|
||||||
|
this._itsatrap.bind(shortcut, () => {
|
||||||
|
const binding = BOOKMARK_BINDINGS[shortcut];
|
||||||
|
this[binding.handler]();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#initializeExistingBookmarkData() {
|
||||||
|
if (!this.existingBookmarkHasReminder || !this.editingExistingBookmark) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prefilledDatetime = this.bookmark.reminderAt;
|
||||||
|
this.bookmark.selectedDatetime = parseCustomDatetime(
|
||||||
|
this.prefilledDatetime,
|
||||||
|
null,
|
||||||
|
this.userTimezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we detect we are bookmarking a post which has local-date data
|
||||||
|
// in it, we can preload that date + time into the form to use as the
|
||||||
|
// bookmark reminder date + time.
|
||||||
|
#loadPostLocalDates() {
|
||||||
|
if (this.bookmark.bookmarkableType !== "Post") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postEl = document.querySelector(
|
||||||
|
`[data-post-id="${this.bookmark.bookmarkableId}"]`
|
||||||
|
);
|
||||||
|
const localDateEl = postEl?.querySelector(".discourse-local-date");
|
||||||
|
|
||||||
|
if (localDateEl) {
|
||||||
|
this.postDetectedLocalDate = localDateEl.dataset.date;
|
||||||
|
this.postDetectedLocalTime = localDateEl.dataset.time;
|
||||||
|
this.postDetectedLocalTimezone = localDateEl.dataset.timezone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,80 +0,0 @@
|
||||||
import Controller from "@ember/controller";
|
|
||||||
import I18n from "I18n";
|
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
|
|
||||||
export function openBookmarkModal(
|
|
||||||
bookmark,
|
|
||||||
callbacks = {
|
|
||||||
onCloseWithoutSaving: null,
|
|
||||||
onAfterSave: null,
|
|
||||||
onAfterDelete: null,
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const model = {
|
|
||||||
id: bookmark.id,
|
|
||||||
reminderAt: bookmark.reminder_at,
|
|
||||||
autoDeletePreference: bookmark.auto_delete_preference,
|
|
||||||
name: bookmark.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
model.bookmarkableId = bookmark.bookmarkable_id;
|
|
||||||
model.bookmarkableType = bookmark.bookmarkable_type;
|
|
||||||
|
|
||||||
let modalController = showModal("bookmark", {
|
|
||||||
model,
|
|
||||||
titleTranslated: I18n.t(
|
|
||||||
bookmark.id ? "bookmarks.edit" : "bookmarks.create"
|
|
||||||
),
|
|
||||||
modalClass: "bookmark-with-reminder",
|
|
||||||
});
|
|
||||||
modalController.setProperties({
|
|
||||||
onCloseWithoutSaving: () => {
|
|
||||||
if (callbacks.onCloseWithoutSaving) {
|
|
||||||
callbacks.onCloseWithoutSaving();
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
afterSave: (savedData) => {
|
|
||||||
let resolveData;
|
|
||||||
if (callbacks.onAfterSave) {
|
|
||||||
resolveData = callbacks.onAfterSave(savedData);
|
|
||||||
}
|
|
||||||
resolve(resolveData);
|
|
||||||
},
|
|
||||||
afterDelete: (topicBookmarked, bookmarkId) => {
|
|
||||||
if (callbacks.onAfterDelete) {
|
|
||||||
callbacks.onAfterDelete(topicBookmarked, bookmarkId);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
|
||||||
onShow() {
|
|
||||||
this.setProperties({
|
|
||||||
model: this.model || {},
|
|
||||||
allowSave: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
|
||||||
registerOnCloseHandler(handlerFn) {
|
|
||||||
this.set("onCloseHandler", handlerFn);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We always want to save the bookmark unless the user specifically
|
|
||||||
* clicks the save or cancel button to mimic browser behaviour.
|
|
||||||
*/
|
|
||||||
onClose(opts = {}) {
|
|
||||||
if (this.onCloseHandler) {
|
|
||||||
this.onCloseHandler(opts);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -10,6 +10,11 @@ import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
||||||
import { isEmpty, isPresent } from "@ember/utils";
|
import { isEmpty, isPresent } from "@ember/utils";
|
||||||
import { next, schedule } from "@ember/runloop";
|
import { next, schedule } from "@ember/runloop";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||||
|
import {
|
||||||
|
CLOSE_INITIATED_BY_BUTTON,
|
||||||
|
CLOSE_INITIATED_BY_ESC,
|
||||||
|
} from "discourse/components/d-modal";
|
||||||
import Bookmark, { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
import Bookmark, { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
import EmberObject, { action } from "@ember/object";
|
import EmberObject, { action } from "@ember/object";
|
||||||
|
@ -29,7 +34,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
import { BookmarkFormData } from "discourse/lib/bookmark";
|
||||||
|
|
||||||
let customPostMessageCallbacks = {};
|
let customPostMessageCallbacks = {};
|
||||||
|
|
||||||
|
@ -53,6 +58,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||||
dialog: service(),
|
dialog: service(),
|
||||||
documentTitle: service(),
|
documentTitle: service(),
|
||||||
screenTrack: service(),
|
screenTrack: service(),
|
||||||
|
modal: service(),
|
||||||
|
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
selectedPostIds: null,
|
selectedPostIds: null,
|
||||||
|
@ -1272,8 +1278,10 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||||
},
|
},
|
||||||
|
|
||||||
_modifyTopicBookmark(bookmark) {
|
_modifyTopicBookmark(bookmark) {
|
||||||
return openBookmarkModal(bookmark, {
|
this.modal.show(BookmarkModal, {
|
||||||
onAfterSave: (savedData) => {
|
model: {
|
||||||
|
bookmark: new BookmarkFormData(bookmark),
|
||||||
|
afterSave: (savedData) => {
|
||||||
this._syncBookmarks(savedData);
|
this._syncBookmarks(savedData);
|
||||||
this.model.set("bookmarking", false);
|
this.model.set("bookmarking", false);
|
||||||
this.model.set("bookmarked", true);
|
this.model.set("bookmarked", true);
|
||||||
|
@ -1284,30 +1292,45 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||||
bookmark.attachedTo()
|
bookmark.attachedTo()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAfterDelete: (topicBookmarked, bookmarkId) => {
|
afterDelete: (topicBookmarked, bookmarkId) => {
|
||||||
this.model.removeBookmark(bookmarkId);
|
this.model.removeBookmark(bookmarkId);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_modifyPostBookmark(bookmark, post) {
|
_modifyPostBookmark(bookmark, post) {
|
||||||
return openBookmarkModal(bookmark, {
|
this.modal
|
||||||
onCloseWithoutSaving: () => {
|
.show(BookmarkModal, {
|
||||||
post.appEvents.trigger("post-stream:refresh", {
|
model: {
|
||||||
id: bookmark.bookmarkable_id,
|
bookmark: new BookmarkFormData(bookmark),
|
||||||
});
|
afterSave: (savedData) => {
|
||||||
},
|
|
||||||
onAfterSave: (savedData) => {
|
|
||||||
this._syncBookmarks(savedData);
|
this._syncBookmarks(savedData);
|
||||||
this.model.set("bookmarking", false);
|
this.model.set("bookmarking", false);
|
||||||
post.createBookmark(savedData);
|
post.createBookmark(savedData);
|
||||||
this.model.afterPostBookmarked(post, savedData);
|
this.model.afterPostBookmarked(post, savedData);
|
||||||
return [post.id];
|
return [post.id];
|
||||||
},
|
},
|
||||||
onAfterDelete: (topicBookmarked, bookmarkId) => {
|
afterDelete: (topicBookmarked, bookmarkId) => {
|
||||||
this.model.removeBookmark(bookmarkId);
|
this.model.removeBookmark(bookmarkId);
|
||||||
post.deleteBookmark(topicBookmarked);
|
post.deleteBookmark(topicBookmarked);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((closeData) => {
|
||||||
|
if (!closeData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
closeData.closeWithoutSaving ||
|
||||||
|
closeData.initiatedBy === CLOSE_INITIATED_BY_ESC ||
|
||||||
|
closeData.initiatedBy === CLOSE_INITIATED_BY_BUTTON
|
||||||
|
) {
|
||||||
|
post.appEvents.trigger("post-stream:refresh", {
|
||||||
|
id: bookmark.bookmarkable_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
||||||
|
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
export function formattedReminderTime(reminderAt, timezone) {
|
export function formattedReminderTime(reminderAt, timezone) {
|
||||||
let reminderAtDate = moment.tz(reminderAt, timezone);
|
let reminderAtDate = moment.tz(reminderAt, timezone);
|
||||||
let formatted = reminderAtDate.format(I18n.t("dates.time"));
|
let formatted = reminderAtDate.format(I18n.t("dates.time"));
|
||||||
|
@ -16,3 +20,43 @@ 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<DModalBody @id="bookmark-reminder-modal">
|
|
||||||
<Bookmark
|
|
||||||
@model={{this.model}}
|
|
||||||
@afterSave={{this.afterSave}}
|
|
||||||
@afterDelete={{this.afterDelete}}
|
|
||||||
@onCloseWithoutSaving={{this.onCloseWithoutSaving}}
|
|
||||||
@registerOnCloseHandler={{action "registerOnCloseHandler"}}
|
|
||||||
@closeModal={{action "closeModal"}}
|
|
||||||
/>
|
|
||||||
</DModalBody>
|
|
|
@ -1,487 +0,0 @@
|
||||||
import {
|
|
||||||
acceptance,
|
|
||||||
exists,
|
|
||||||
loggedInUser,
|
|
||||||
query,
|
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
|
||||||
import I18n from "I18n";
|
|
||||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import topicFixtures from "discourse/tests/fixtures/topic";
|
|
||||||
import { cloneJSON } from "discourse-common/lib/object";
|
|
||||||
|
|
||||||
async function openBookmarkModal(postNumber = 1) {
|
|
||||||
if (exists(`#post_${postNumber} button.show-more-actions`)) {
|
|
||||||
await click(`#post_${postNumber} button.show-more-actions`);
|
|
||||||
}
|
|
||||||
await click(`#post_${postNumber} button.bookmark`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openEditBookmarkModal() {
|
|
||||||
await click(".topic-post:first-child button.bookmarked");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testTopicLevelBookmarkButtonIcon(assert, postNumber) {
|
|
||||||
const iconWithoutClock = "d-icon-bookmark";
|
|
||||||
const iconWithClock = "d-icon-discourse-bookmark-clock";
|
|
||||||
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
assert.ok(
|
|
||||||
query("#topic-footer-button-bookmark svg").classList.contains(
|
|
||||||
iconWithoutClock
|
|
||||||
),
|
|
||||||
"Shows an icon without a clock when there is no a bookmark"
|
|
||||||
);
|
|
||||||
|
|
||||||
await openBookmarkModal(postNumber);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
query("#topic-footer-button-bookmark svg").classList.contains(
|
|
||||||
iconWithoutClock
|
|
||||||
),
|
|
||||||
"Shows an icon without a clock when there is a bookmark without a reminder"
|
|
||||||
);
|
|
||||||
|
|
||||||
await openBookmarkModal(postNumber);
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
query("#topic-footer-button-bookmark svg").classList.contains(
|
|
||||||
iconWithClock
|
|
||||||
),
|
|
||||||
"Shows an icon with a clock when there is a bookmark with a reminder"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptance("Bookmarking", function (needs) {
|
|
||||||
needs.user();
|
|
||||||
|
|
||||||
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
|
|
||||||
topicResponse.post_stream.posts[0].cooked += `<span data-date="2036-01-15" data-time="00:35:00" class="discourse-local-date cooked-date past" data-timezone="Europe/London">
|
|
||||||
<span>
|
|
||||||
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<use href="#globe-americas"></use>
|
|
||||||
</svg>
|
|
||||||
<span class="relative-time">January 15, 2036 12:35 AM</span>
|
|
||||||
</span>
|
|
||||||
</span>`;
|
|
||||||
|
|
||||||
topicResponse.post_stream.posts[1].cooked += `<span data-date="2021-01-15" data-time="00:35:00" class="discourse-local-date cooked-date past" data-timezone="Europe/London">
|
|
||||||
<span>
|
|
||||||
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<use href="#globe-americas"></use>
|
|
||||||
</svg>
|
|
||||||
<span class="relative-time">Today 10:30 AM</span>
|
|
||||||
</span>
|
|
||||||
</span>`;
|
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
function handleRequest(request) {
|
|
||||||
const data = helper.parsePostData(request.requestBody);
|
|
||||||
|
|
||||||
if (data.bookmarkable_id === "398" && data.bookmarkable_type === "Post") {
|
|
||||||
return helper.response({ id: 1, success: "OK" });
|
|
||||||
} else if (data.bookmarkable_type === "Topic") {
|
|
||||||
return helper.response({ id: 3, success: "OK" });
|
|
||||||
} else if (
|
|
||||||
data.bookmarkable_id === "419" &&
|
|
||||||
data.bookmarkable_type === "Post"
|
|
||||||
) {
|
|
||||||
return helper.response({ id: 2, success: "OK" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
server.post("/bookmarks", handleRequest);
|
|
||||||
server.put("/bookmarks/1", handleRequest);
|
|
||||||
server.put("/bookmarks/2", handleRequest);
|
|
||||||
server.put("/bookmarks/3", handleRequest);
|
|
||||||
server.delete("/bookmarks/1", () =>
|
|
||||||
helper.response({ success: "OK", topic_bookmarked: false })
|
|
||||||
);
|
|
||||||
server.get("/t/280.json", () => helper.response(topicResponse));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Bookmarks modal opening", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
assert.ok(
|
|
||||||
exists("#bookmark-reminder-modal"),
|
|
||||||
"it shows the bookmark modal"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
exists("#tap_tile_none.active"),
|
|
||||||
"it highlights the None option by default"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Bookmarks modal selecting reminder type", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#tap_tile_start_of_next_business_week");
|
|
||||||
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#tap_tile_next_month");
|
|
||||||
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#tap_tile_custom");
|
|
||||||
assert.ok(exists("#tap_tile_custom.active"), "it selects custom");
|
|
||||||
assert.ok(exists(".tap-tile-date-input"), "it shows the custom date input");
|
|
||||||
assert.ok(exists(".tap-tile-time-input"), "it shows the custom time input");
|
|
||||||
await click("#save-bookmark");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Saving a bookmark with a reminder", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await fillIn("input#bookmark-name", "Check this out later");
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"it shows the bookmarked icon on the post"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
exists(
|
|
||||||
".topic-post:first-child button.bookmark.bookmarked > .d-icon-discourse-bookmark-clock"
|
|
||||||
),
|
|
||||||
"it shows the bookmark clock icon because of the reminder"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Opening the options panel and remembering the option", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
assert.notOk(
|
|
||||||
exists(".bookmark-options-panel"),
|
|
||||||
"it should not open the options panel by default"
|
|
||||||
);
|
|
||||||
await click(".bookmark-options-button");
|
|
||||||
assert.ok(
|
|
||||||
exists(".bookmark-options-panel"),
|
|
||||||
"it should open the options panel"
|
|
||||||
);
|
|
||||||
await selectKit(".bookmark-option-selector").expand();
|
|
||||||
await selectKit(".bookmark-option-selector").selectRowByValue(1);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
await openEditBookmarkModal();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".bookmark-options-panel"),
|
|
||||||
"it should reopen the options panel"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
selectKit(".bookmark-option-selector").header().value(),
|
|
||||||
"1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Saving a bookmark with no reminder or name", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"it shows the bookmarked icon on the post"
|
|
||||||
);
|
|
||||||
assert.notOk(
|
|
||||||
exists(
|
|
||||||
".topic-post:first-child button.bookmark.bookmarked > .d-icon-discourse-bookmark-clock"
|
|
||||||
),
|
|
||||||
"it shows the regular bookmark active icon"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Deleting a bookmark with a reminder", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
await openEditBookmarkModal();
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists("#bookmark-reminder-modal"),
|
|
||||||
"it shows the bookmark modal"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("#delete-bookmark");
|
|
||||||
|
|
||||||
assert.ok(exists(".dialog-body"), "it asks for delete confirmation");
|
|
||||||
assert.ok(
|
|
||||||
query(".dialog-body").innerText.includes(
|
|
||||||
I18n.t("bookmarks.confirm_delete")
|
|
||||||
),
|
|
||||||
"it shows delete confirmation message"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".dialog-footer .btn-danger");
|
|
||||||
|
|
||||||
assert.notOk(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"it no longer shows the bookmarked icon on the post after bookmark is deleted"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Cancelling saving a bookmark", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await click(".d-modal-cancel");
|
|
||||||
assert.notOk(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"it does not show the bookmarked icon on the post because it is not saved"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Editing a bookmark", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
let now = moment.tz(loggedInUser().user_option.timezone);
|
|
||||||
let tomorrow = now.add(1, "day").format("YYYY-MM-DD");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await fillIn("input#bookmark-name", "Test name");
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
await openEditBookmarkModal();
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#bookmark-name").value,
|
|
||||||
"Test name",
|
|
||||||
"it should prefill the bookmark name"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#custom-date > input").value,
|
|
||||||
tomorrow,
|
|
||||||
"it should prefill the bookmark date"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#custom-time").value,
|
|
||||||
"08:00",
|
|
||||||
"it should prefill the bookmark time"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Using a post date for the reminder date", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
let postDate = moment.tz("2036-01-15", loggedInUser().user_option.timezone);
|
|
||||||
let postDateFormatted = postDate.format("YYYY-MM-DD");
|
|
||||||
await openBookmarkModal();
|
|
||||||
await fillIn("input#bookmark-name", "Test name");
|
|
||||||
await click("#tap_tile_post_local_date");
|
|
||||||
|
|
||||||
await openEditBookmarkModal();
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#bookmark-name").value,
|
|
||||||
"Test name",
|
|
||||||
"it should prefill the bookmark name"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#custom-date > input").value,
|
|
||||||
postDateFormatted,
|
|
||||||
"it should prefill the bookmark date"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#custom-time").value,
|
|
||||||
"10:35",
|
|
||||||
"it should prefill the bookmark time"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Cannot use the post date for a reminder when the post date is in the past", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal(2);
|
|
||||||
assert.notOk(
|
|
||||||
exists("#tap_tile_post_local_date"),
|
|
||||||
"it does not show the local date tile"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("The topic level bookmark button deletes all bookmarks if several posts on the topic are bookmarked", async function (assert) {
|
|
||||||
const yesButton = ".dialog-footer .btn-primary";
|
|
||||||
const noButton = ".dialog-footer .btn-default";
|
|
||||||
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal(1);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
await openBookmarkModal(2);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first bookmark is added"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:nth-child(3) button.bookmark.bookmarked"),
|
|
||||||
"the second bookmark is added"
|
|
||||||
);
|
|
||||||
|
|
||||||
// open the modal and cancel deleting
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click(noButton);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first bookmark isn't deleted"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:nth-child(3) button.bookmark.bookmarked"),
|
|
||||||
"the second bookmark isn't deleted"
|
|
||||||
);
|
|
||||||
|
|
||||||
// open the modal and accept deleting
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click(yesButton);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
!exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first bookmark is deleted"
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
!exists(".topic-post:nth-child(3) button.bookmark.bookmarked"),
|
|
||||||
"the second bookmark is deleted"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("The topic level bookmark button opens the edit modal if only the first post on the topic is bookmarked", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal(1);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.edit_bookmark"),
|
|
||||||
"A topic level bookmark button has a label 'Edit Bookmark'"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists("div.modal.bookmark-with-reminder"),
|
|
||||||
"The edit modal is opened"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Creating and editing a topic level bookmark", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.notOk(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first post is not marked as being bookmarked"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.edit_bookmark"),
|
|
||||||
"A topic level bookmark button has a label 'Edit Bookmark'"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await fillIn("input#bookmark-name", "Test name");
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("input#bookmark-name").value,
|
|
||||||
"Test name",
|
|
||||||
"The topic level bookmark editing preserves the values entered"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".d-modal-cancel");
|
|
||||||
|
|
||||||
await openBookmarkModal(1);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first post is bookmarked independently of the topic level bookmark"
|
|
||||||
);
|
|
||||||
|
|
||||||
// deleting all bookmarks in the topic
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.clear_bookmarks"),
|
|
||||||
"the footer button says Clear Bookmarks because there is more than one"
|
|
||||||
);
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click(".dialog-footer .btn-primary");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
!exists(".topic-post:first-child button.bookmark.bookmarked"),
|
|
||||||
"the first post bookmark is deleted"
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.title"),
|
|
||||||
"the topic level bookmark is deleted"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Deleting a topic_level bookmark with a reminder", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.edit_bookmark"),
|
|
||||||
"A topic level bookmark button has a label 'Edit Bookmark'"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await fillIn("input#bookmark-name", "Test name");
|
|
||||||
await click("#tap_tile_tomorrow");
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
await click("#delete-bookmark");
|
|
||||||
|
|
||||||
assert.ok(exists(".dialog-body"), "it asks for delete confirmation");
|
|
||||||
assert.ok(
|
|
||||||
query(".dialog-body").innerText.includes(
|
|
||||||
I18n.t("bookmarks.confirm_delete")
|
|
||||||
),
|
|
||||||
"it shows delete confirmation message"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".dialog-footer .btn-danger");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.title"),
|
|
||||||
"A topic level bookmark button no longer says 'Edit Bookmark' after deletion"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("The topic level bookmark button opens the edit modal if only one post in the post stream is bookmarked", async function (assert) {
|
|
||||||
await visit("/t/internationalization-localization/280");
|
|
||||||
await openBookmarkModal(2);
|
|
||||||
await click("#save-bookmark");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#topic-footer-button-bookmark").innerText,
|
|
||||||
I18n.t("bookmarked.edit_bookmark"),
|
|
||||||
"A topic level bookmark button has a label 'Edit Bookmark'"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("#topic-footer-button-bookmark");
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists("div.modal.bookmark-with-reminder"),
|
|
||||||
"The edit modal is opened"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the first post", async function (assert) {
|
|
||||||
const postNumber = 1;
|
|
||||||
await testTopicLevelBookmarkButtonIcon(assert, postNumber);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the second post", async function (assert) {
|
|
||||||
const postNumber = 2;
|
|
||||||
await testTopicLevelBookmarkButtonIcon(assert, postNumber);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { module, test } from "qunit";
|
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
||||||
import { render } from "@ember/test-helpers";
|
|
||||||
import { exists } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
|
||||||
|
|
||||||
module("Integration | Component | bookmark-alert", function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.setProperties({
|
|
||||||
model: {},
|
|
||||||
closeModal: () => {},
|
|
||||||
afterSave: () => {},
|
|
||||||
afterDelete: () => {},
|
|
||||||
registerOnCloseHandler: () => {},
|
|
||||||
onCloseWithoutSaving: () => {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("alert exists for reminder in the future", async function (assert) {
|
|
||||||
let name = "test";
|
|
||||||
let futureDate = new Date();
|
|
||||||
futureDate.setDate(futureDate.getDate() + 10);
|
|
||||||
|
|
||||||
let reminderAt = futureDate.toISOString();
|
|
||||||
this.model = { id: 1, name, reminderAt };
|
|
||||||
|
|
||||||
await render(hbs`
|
|
||||||
<Bookmark
|
|
||||||
@model={{this.model}}
|
|
||||||
@afterSave={{this.afterSave}}
|
|
||||||
@afterDelete={{this.afterDelete}}
|
|
||||||
@onCloseWithoutSaving={{this.onCloseWithoutSaving}}
|
|
||||||
@registerOnCloseHandler={{this.registerOnCloseHandler}}
|
|
||||||
@closeModal={{this.closeModal}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
exists(".existing-reminder-at-alert"),
|
|
||||||
"alert found for future reminder"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("alert does not exist for reminder in the past", async function (assert) {
|
|
||||||
let name = "test";
|
|
||||||
let pastDate = new Date();
|
|
||||||
pastDate.setDate(pastDate.getDate() - 1);
|
|
||||||
|
|
||||||
let reminderAt = pastDate.toISOString();
|
|
||||||
this.model = { id: 1, name, reminderAt };
|
|
||||||
|
|
||||||
await render(hbs`
|
|
||||||
<Bookmark
|
|
||||||
@model={{this.model}}
|
|
||||||
@afterSave={{this.afterSave}}
|
|
||||||
@afterDelete={{this.afterDelete}}
|
|
||||||
@onCloseWithoutSaving={{this.onCloseWithoutSaving}}
|
|
||||||
@registerOnCloseHandler={{this.registerOnCloseHandler}}
|
|
||||||
@closeModal={{this.closeModal}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
!exists(".existing-reminder-at-alert"),
|
|
||||||
"alert not found for past reminder"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { module, test } from "qunit";
|
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
|
||||||
import { render } from "@ember/test-helpers";
|
|
||||||
import { query } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
|
||||||
import I18n from "I18n";
|
|
||||||
|
|
||||||
module("Integration | Component | bookmark", function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.setProperties({
|
|
||||||
model: {},
|
|
||||||
closeModal: () => {},
|
|
||||||
afterSave: () => {},
|
|
||||||
afterDelete: () => {},
|
|
||||||
registerOnCloseHandler: () => {},
|
|
||||||
onCloseWithoutSaving: () => {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("prefills the custom reminder type date and time", async function (assert) {
|
|
||||||
let name = "test";
|
|
||||||
let reminderAt = "2020-05-15T09:45:00";
|
|
||||||
this.model = { id: 1, autoDeletePreference: 0, name, reminderAt };
|
|
||||||
|
|
||||||
await render(hbs`
|
|
||||||
<Bookmark
|
|
||||||
@model={{this.model}}
|
|
||||||
@afterSave={{this.afterSave}}
|
|
||||||
@afterDelete={{this.afterDelete}}
|
|
||||||
@onCloseWithoutSaving={{this.onCloseWithoutSaving}}
|
|
||||||
@registerOnCloseHandler={{this.registerOnCloseHandler}}
|
|
||||||
@closeModal={{this.closeModal}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
|
|
||||||
assert.strictEqual(query("#bookmark-name").value, "test");
|
|
||||||
assert.strictEqual(
|
|
||||||
query("#custom-date > .date-picker").value,
|
|
||||||
"2020-05-15"
|
|
||||||
);
|
|
||||||
assert.strictEqual(query("#custom-time").value, "09:45");
|
|
||||||
assert.strictEqual(
|
|
||||||
query(".selected-name > .name").innerHTML.trim(),
|
|
||||||
I18n.t("bookmarks.auto_delete_preference.never")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -3,16 +3,22 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 310px;
|
min-width: 310px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
padding: 10px 0;
|
padding: 0 1em 1em 1em;
|
||||||
|
|
||||||
.delete-bookmark {
|
.delete-bookmark {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-inner-container {
|
||||||
|
max-width: 375px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
width: 375px;
|
width: 375px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -123,6 +123,10 @@ class Bookmark < ActiveRecord::Base
|
||||||
update!(reminder_last_sent_at: Time.zone.now, reminder_set_at: nil)
|
update!(reminder_last_sent_at: Time.zone.now, reminder_set_at: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reminder_at_in_zone(timezone)
|
||||||
|
self.reminder_at.in_time_zone(timezone)
|
||||||
|
end
|
||||||
|
|
||||||
scope :with_reminders, -> { where("reminder_at IS NOT NULL") }
|
scope :with_reminders, -> { where("reminder_at IS NOT NULL") }
|
||||||
|
|
||||||
scope :pending_reminders,
|
scope :pending_reminders,
|
||||||
|
|
|
@ -61,6 +61,10 @@ module SiteIconManager
|
||||||
|
|
||||||
WATCHED_SETTINGS = ICONS.keys + %i[logo logo_small]
|
WATCHED_SETTINGS = ICONS.keys + %i[logo logo_small]
|
||||||
|
|
||||||
|
def self.clear_cache!
|
||||||
|
@cache.clear
|
||||||
|
end
|
||||||
|
|
||||||
def self.ensure_optimized!
|
def self.ensure_optimized!
|
||||||
unless @disabled
|
unless @disabled
|
||||||
ICONS.each do |name, info|
|
ICONS.each do |name, info|
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { bind } from "discourse-common/utils/decorators";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
||||||
import Bookmark from "discourse/models/bookmark";
|
import Bookmark from "discourse/models/bookmark";
|
||||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||||
|
import { BookmarkFormData } from "discourse/lib/bookmark";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
@ -42,6 +43,7 @@ export default class ChatMessageInteractor {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service site;
|
@service site;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service modal;
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
|
|
||||||
@tracked message = null;
|
@tracked message = null;
|
||||||
|
@ -305,11 +307,17 @@ export default class ChatMessageInteractor {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleBookmark() {
|
toggleBookmark() {
|
||||||
return openBookmarkModal(
|
this.modal.show(BookmarkModal, {
|
||||||
|
model: {
|
||||||
|
bookmark: new BookmarkFormData(
|
||||||
this.message.bookmark ||
|
this.message.bookmark ||
|
||||||
Bookmark.createFor(this.currentUser, "Chat::Message", this.message.id),
|
Bookmark.createFor(
|
||||||
{
|
this.currentUser,
|
||||||
onAfterSave: (savedData) => {
|
"Chat::Message",
|
||||||
|
this.message.id
|
||||||
|
)
|
||||||
|
),
|
||||||
|
afterSave: (savedData) => {
|
||||||
const bookmark = Bookmark.create(savedData);
|
const bookmark = Bookmark.create(savedData);
|
||||||
this.message.bookmark = bookmark;
|
this.message.bookmark = bookmark;
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
|
@ -318,11 +326,11 @@ export default class ChatMessageInteractor {
|
||||||
bookmark.attachedTo()
|
bookmark.attachedTo()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAfterDelete: () => {
|
afterDelete: () => {
|
||||||
this.message.bookmark = null;
|
this.message.bookmark = null;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -2,19 +2,27 @@
|
||||||
|
|
||||||
describe "Local dates", type: :system do
|
describe "Local dates", type: :system do
|
||||||
fab!(:topic) { Fabricate(:topic) }
|
fab!(:topic) { Fabricate(:topic) }
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
let(:year) { Time.zone.now.year + 1 }
|
||||||
|
let(:bookmark_modal) { PageObjects::Modals::Bookmark.new }
|
||||||
|
|
||||||
before { create_post(user: user, topic: topic, title: "Date range test post", raw: <<~RAW) }
|
before do
|
||||||
First option: [date=2022-12-15 time=14:19:00 timezone="Asia/Singapore"]
|
create_post(user: current_user, topic: topic, title: "Date range test post", raw: <<~RAW)
|
||||||
Second option: [date=2022-12-15 time=01:20:00 timezone="Asia/Singapore"], or [date=2022-12-15 time=02:40:00 timezone="Asia/Singapore"]
|
First option: [date=#{year}-12-15 time=14:19:00 timezone="Asia/Singapore"]
|
||||||
Third option: [date-range from=2022-12-15T11:25:00 to=2022-12-16T00:26:00 timezone="Asia/Singapore"] or [date-range from=2022-12-22T11:57:00 to=2022-12-23T11:58:00 timezone="Asia/Singapore"]
|
Second option: [date=#{year}-12-15 time=01:20:00 timezone="Asia/Singapore"], or [date=#{year}-12-15 time=02:40:00 timezone="Asia/Singapore"]
|
||||||
|
Third option: [date-range from=#{year}-12-15T11:25:00 to=#{year}-12-16T00:26:00 timezone="Asia/Singapore"] or [date-range from=#{year}-12-22T11:57:00 to=#{year}-12-23T11:58:00 timezone="Asia/Singapore"]
|
||||||
RAW
|
RAW
|
||||||
|
end
|
||||||
|
|
||||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||||
|
|
||||||
|
def formatted_date_for_year(month, day)
|
||||||
|
Date.parse("#{year}-#{month}-#{day}").strftime("%A, %B %-d, %Y")
|
||||||
|
end
|
||||||
|
|
||||||
it "renders local dates and date ranges correctly" do
|
it "renders local dates and date ranges correctly" do
|
||||||
using_browser_timezone("Asia/Singapore") do
|
using_browser_timezone("Asia/Singapore") do
|
||||||
sign_in user
|
sign_in current_user
|
||||||
|
|
||||||
topic_page.visit_topic(topic)
|
topic_page.visit_topic(topic)
|
||||||
|
|
||||||
|
@ -27,19 +35,19 @@ describe "Local dates", type: :system do
|
||||||
post_dates[0].click
|
post_dates[0].click
|
||||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||||
|
|
||||||
expect(tippy_date).to have_text("Thursday, December 15, 2022\n2:19 PM", exact: true)
|
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n2:19 PM", exact: true)
|
||||||
|
|
||||||
# Two single dates in the same paragraph.
|
# Two single dates in the same paragraph.
|
||||||
#
|
#
|
||||||
post_dates[1].click
|
post_dates[1].click
|
||||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||||
|
|
||||||
expect(tippy_date).to have_text("Thursday, December 15, 2022\n1:20 AM", exact: true)
|
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n1:20 AM", exact: true)
|
||||||
|
|
||||||
post_dates[2].click
|
post_dates[2].click
|
||||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||||
|
|
||||||
expect(tippy_date).to have_text("Thursday, December 15, 2022\n2:40 AM", exact: true)
|
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n2:40 AM", exact: true)
|
||||||
|
|
||||||
# Two date ranges in the same paragraph.
|
# Two date ranges in the same paragraph.
|
||||||
#
|
#
|
||||||
|
@ -47,7 +55,7 @@ describe "Local dates", type: :system do
|
||||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||||
|
|
||||||
expect(tippy_date).to have_text(
|
expect(tippy_date).to have_text(
|
||||||
"Thursday, December 15, 2022\n11:25 AM → 12:26 AM",
|
"#{formatted_date_for_year(12, 15)}\n11:25 AM → 12:26 AM",
|
||||||
exact: true,
|
exact: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,9 +63,38 @@ describe "Local dates", type: :system do
|
||||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||||
|
|
||||||
expect(tippy_date).to have_text(
|
expect(tippy_date).to have_text(
|
||||||
"Thursday, December 22, 2022 11:57 AM → Friday, December 23, 2022 11:58 AM",
|
"#{formatted_date_for_year(12, 22)} 11:57 AM → #{formatted_date_for_year(12, 23)} 11:58 AM",
|
||||||
exact: true,
|
exact: true,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "bookmarks" do
|
||||||
|
before do
|
||||||
|
current_user.user_option.update!(timezone: "Asia/Singapore")
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can use the post local date for a bookmark preset" 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_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)
|
||||||
|
expect(bookmark.reminder_at.to_s).to eq("#{year}-12-15 06:19:00 UTC")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not allow using post dates in the past for a bookmark preset" do
|
||||||
|
topic.first_post.update!(
|
||||||
|
raw: 'First option: [date=1999-12-15 time=14:19:00 timezone="Asia/Singapore"]',
|
||||||
|
)
|
||||||
|
topic.first_post.rebake!
|
||||||
|
topic_page.visit_topic(topic)
|
||||||
|
topic_page.expand_post_actions(topic.first_post)
|
||||||
|
topic_page.click_post_action_button(topic.first_post, :bookmark)
|
||||||
|
expect(bookmark_modal).to be_open
|
||||||
|
expect(bookmark_modal).to have_no_preset(:post_local_date)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -322,6 +322,9 @@ RSpec.configure do |config|
|
||||||
else
|
else
|
||||||
DB.exec "SELECT setval('uploads_id_seq', 1)"
|
DB.exec "SELECT setval('uploads_id_seq', 1)"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Prevents 500 errors for site setting URLs pointing to test.localhost in system specs.
|
||||||
|
SiteIconManager.clear_cache!
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestLocalProcessProvider < SiteSettings::LocalProcessProvider
|
class TestLocalProcessProvider < SiteSettings::LocalProcessProvider
|
||||||
|
|
|
@ -2,14 +2,18 @@
|
||||||
|
|
||||||
describe "Bookmarking posts and topics", type: :system do
|
describe "Bookmarking posts and topics", type: :system do
|
||||||
fab!(:topic) { Fabricate(:topic) }
|
fab!(:topic) { Fabricate(:topic) }
|
||||||
fab!(:user) { Fabricate(:user) }
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
fab!(:post) { Fabricate(:post, topic: topic, raw: "This is some post to bookmark") }
|
fab!(:post) { Fabricate(:post, topic: topic, raw: "This is some post to bookmark") }
|
||||||
fab!(:post2) { Fabricate(:post, topic: topic, raw: "Some interesting post content") }
|
fab!(:post_2) { Fabricate(:post, topic: topic, raw: "Some interesting post content") }
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
before { sign_in user }
|
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_modal(post)
|
||||||
topic_page.visit_topic(topic)
|
topic_page.visit_topic(topic)
|
||||||
|
@ -24,15 +28,15 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
bookmark_modal.save
|
bookmark_modal.save
|
||||||
|
|
||||||
expect(topic_page).to have_post_bookmarked(post)
|
expect(topic_page).to have_post_bookmarked(post)
|
||||||
bookmark = Bookmark.find_by(bookmarkable: post, user: user)
|
bookmark = Bookmark.find_by(bookmarkable: post, user: current_user)
|
||||||
expect(bookmark.name).to eq("something important")
|
expect(bookmark.name).to eq("something important")
|
||||||
expect(bookmark.reminder_at).to eq(nil)
|
expect(bookmark.reminder_at).to eq(nil)
|
||||||
|
|
||||||
visit_topic_and_open_bookmark_modal(post2)
|
visit_topic_and_open_bookmark_modal(post_2)
|
||||||
|
|
||||||
bookmark_modal.select_preset_reminder(:tomorrow)
|
bookmark_modal.select_preset_reminder(:tomorrow)
|
||||||
expect(topic_page).to have_post_bookmarked(post2)
|
expect(topic_page).to have_post_bookmarked(post_2)
|
||||||
bookmark = Bookmark.find_by(bookmarkable: post2, user: user)
|
bookmark = Bookmark.find_by(bookmarkable: post_2, user: current_user)
|
||||||
expect(bookmark.reminder_at).not_to eq(nil)
|
expect(bookmark.reminder_at).not_to eq(nil)
|
||||||
expect(bookmark.reminder_set_at).not_to eq(nil)
|
expect(bookmark.reminder_set_at).not_to eq(nil)
|
||||||
end
|
end
|
||||||
|
@ -44,7 +48,7 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
bookmark_modal.cancel
|
bookmark_modal.cancel
|
||||||
|
|
||||||
expect(topic_page).to have_no_post_bookmarked(post)
|
expect(topic_page).to have_no_post_bookmarked(post)
|
||||||
expect(Bookmark.exists?(bookmarkable: post, user: user)).to eq(false)
|
expect(Bookmark.exists?(bookmarkable: post, user: current_user)).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates a bookmark if the modal is closed by clicking outside the modal window" do
|
it "creates a bookmark if the modal is closed by clicking outside the modal window" do
|
||||||
|
@ -56,6 +60,26 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
expect(topic_page).to have_post_bookmarked(post)
|
expect(topic_page).to have_post_bookmarked(post)
|
||||||
end
|
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)
|
||||||
|
bookmark_modal.open_options_panel
|
||||||
|
expect(bookmark_modal).to have_auto_delete_preference(
|
||||||
|
Bookmark.auto_delete_preferences[:on_owner_reply],
|
||||||
|
)
|
||||||
|
bookmark_modal.select_auto_delete_preference(Bookmark.auto_delete_preferences[:clear_reminder])
|
||||||
|
bookmark_modal.save
|
||||||
|
expect(topic_page).to have_post_bookmarked(post_2)
|
||||||
|
topic_page.click_post_action_button(post_2, :bookmark)
|
||||||
|
expect(bookmark_modal).to have_open_options_panel
|
||||||
|
expect(bookmark_modal).to have_auto_delete_preference(
|
||||||
|
Bookmark.auto_delete_preferences[:clear_reminder],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "topic level bookmarks" do
|
||||||
it "allows the topic to be bookmarked" do
|
it "allows the topic to be bookmarked" do
|
||||||
topic_page.visit_topic(topic)
|
topic_page.visit_topic(topic)
|
||||||
topic_page.click_topic_footer_button(:bookmark)
|
topic_page.click_topic_footer_button(:bookmark)
|
||||||
|
@ -64,14 +88,80 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
bookmark_modal.save
|
bookmark_modal.save
|
||||||
|
|
||||||
expect(topic_page).to have_topic_bookmarked
|
expect(topic_page).to have_topic_bookmarked
|
||||||
bookmark =
|
expect(Bookmark.exists?(bookmarkable: topic, user: current_user)).to be_truthy
|
||||||
try_until_success { expect(Bookmark.exists?(bookmarkable: topic, user: user)).to eq(true) }
|
end
|
||||||
expect(bookmark).not_to eq(nil)
|
|
||||||
|
it "opens the edit bookmark modal from the topic bookmark button if one post is bookmarked" do
|
||||||
|
bookmark = Fabricate(:bookmark, bookmarkable: post_2, user: current_user)
|
||||||
|
topic_page.visit_topic(topic)
|
||||||
|
topic_page.click_topic_footer_button(:bookmark)
|
||||||
|
expect(bookmark_modal).to be_open
|
||||||
|
expect(bookmark_modal).to be_editing_id(bookmark.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "clears all topic bookmarks from the topic bookmark button if more than one post is bookmarked" do
|
||||||
|
Fabricate(:bookmark, bookmarkable: post, user: current_user)
|
||||||
|
Fabricate(:bookmark, bookmarkable: post_2, user: current_user)
|
||||||
|
topic_page.visit_topic(topic)
|
||||||
|
topic_page.click_topic_footer_button(:bookmark)
|
||||||
|
dialog = PageObjects::Components::Dialog.new
|
||||||
|
expect(dialog).to have_content(I18n.t("js.bookmarks.confirm_clear"))
|
||||||
|
dialog.click_yes
|
||||||
|
expect(dialog).to be_closed
|
||||||
|
expect(Bookmark.where(user: current_user).count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "editing existing bookmarks" do
|
||||||
|
fab!(:bookmark) do
|
||||||
|
Fabricate(
|
||||||
|
:bookmark,
|
||||||
|
bookmarkable: post_2,
|
||||||
|
user: current_user,
|
||||||
|
name: "test name",
|
||||||
|
reminder_at: 10.days.from_now,
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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(
|
||||||
|
bookmark_modal.existing_reminder_alert_message(bookmark),
|
||||||
|
)
|
||||||
|
expect(bookmark_modal.custom_date_picker.value).to eq(
|
||||||
|
bookmark.reminder_at_in_zone(timezone).strftime("%Y-%m-%d"),
|
||||||
|
)
|
||||||
|
expect(bookmark_modal.custom_time_picker.value).to eq(
|
||||||
|
bookmark.reminder_at_in_zone(timezone).strftime("%H:%M"),
|
||||||
|
)
|
||||||
|
expect(bookmark_modal).to have_active_preset("custom")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can delete the bookmark" do
|
||||||
|
topic_page.visit_topic(topic)
|
||||||
|
topic_page.click_post_action_button(post_2, :bookmark)
|
||||||
|
bookmark_modal.delete
|
||||||
|
bookmark_modal.confirm_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)
|
||||||
|
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")
|
||||||
|
expect(bookmark.reload.name).to eq("test name")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the user has a bookmark auto_delete_preference" do
|
context "when the user has a bookmark auto_delete_preference" do
|
||||||
before do
|
before do
|
||||||
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],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -82,7 +172,7 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
bookmark_modal.save
|
bookmark_modal.save
|
||||||
expect(topic_page).to have_post_bookmarked(post)
|
expect(topic_page).to have_post_bookmarked(post)
|
||||||
|
|
||||||
bookmark = Bookmark.find_by(bookmarkable: post, user: user)
|
bookmark = Bookmark.find_by(bookmarkable: post, user: current_user)
|
||||||
expect(bookmark.auto_delete_preference).to eq(
|
expect(bookmark.auto_delete_preference).to eq(
|
||||||
Bookmark.auto_delete_preferences[:on_owner_reply],
|
Bookmark.auto_delete_preferences[:on_owner_reply],
|
||||||
)
|
)
|
||||||
|
@ -94,7 +184,7 @@ describe "Bookmarking posts and topics", type: :system do
|
||||||
bookmark_modal.save
|
bookmark_modal.save
|
||||||
expect(topic_page).to have_post_bookmarked(post)
|
expect(topic_page).to have_post_bookmarked(post)
|
||||||
|
|
||||||
bookmark = Bookmark.find_by(bookmarkable: post, user: user)
|
bookmark = Bookmark.find_by(bookmarkable: post, user: current_user)
|
||||||
expect(bookmark.auto_delete_preference).to eq(
|
expect(bookmark.auto_delete_preference).to eq(
|
||||||
Bookmark.auto_delete_preferences[:on_owner_reply],
|
Bookmark.auto_delete_preferences[:on_owner_reply],
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Components
|
module Components
|
||||||
class Dialog < PageObjects::Components::Base
|
class Dialog < PageObjects::Components::Base
|
||||||
|
def closed?
|
||||||
|
has_no_css?(".dialog-container")
|
||||||
|
end
|
||||||
|
|
||||||
|
def open?
|
||||||
|
has_css?(".dialog-container")
|
||||||
|
end
|
||||||
|
|
||||||
def has_content?(content)
|
def has_content?(content)
|
||||||
find(".dialog-container").has_content?(content)
|
find(".dialog-container").has_content?(content)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,16 +4,90 @@ module PageObjects
|
||||||
module Modals
|
module Modals
|
||||||
class Bookmark < PageObjects::Modals::Base
|
class Bookmark < PageObjects::Modals::Base
|
||||||
def fill_name(name)
|
def fill_name(name)
|
||||||
fill_in "bookmark-name", with: name
|
fill_in("bookmark-name", with: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def name
|
||||||
|
find("#bookmark-name")
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_preset_reminder(identifier)
|
def select_preset_reminder(identifier)
|
||||||
find("#tap_tile_#{identifier}").click
|
find("#tap_tile_#{identifier}").click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_active_preset?(identifier)
|
||||||
|
has_css?("#tap_tile_#{identifier}.tap-tile.active")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_preset?(identifier)
|
||||||
|
has_css?("#tap_tile_#{identifier}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_no_preset?(identifier)
|
||||||
|
has_no_css?("#tap_tile_#{identifier}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def editing_id?(bookmark_id)
|
||||||
|
has_css?(".bookmark-reminder-modal[data-bookmark-id='#{bookmark_id}']")
|
||||||
|
end
|
||||||
|
|
||||||
|
def open_options_panel
|
||||||
|
find(".bookmark-options-button").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_open_options_panel?
|
||||||
|
has_css?(".bookmark-options-panel")
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_auto_delete_preference(preference)
|
||||||
|
select_kit = PageObjects::Components::SelectKit.new("#bookmark-auto-delete-preference")
|
||||||
|
select_kit.expand
|
||||||
|
select_kit.select_row_by_value(preference)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_auto_delete_preference?(preference)
|
||||||
|
select_kit = PageObjects::Components::SelectKit.new("#bookmark-auto-delete-preference")
|
||||||
|
select_kit.has_selected_value?(preference)
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_date_picker
|
||||||
|
find(".tap-tile-date-input #custom-date .date-picker")
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_time_picker
|
||||||
|
find(".tap-tile-time-input #custom-time")
|
||||||
|
end
|
||||||
|
|
||||||
def save
|
def save
|
||||||
find("#save-bookmark").click
|
find("#save-bookmark").click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
find("#delete-bookmark").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_delete
|
||||||
|
find(".dialog-footer .btn-danger").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_reminder_alert
|
||||||
|
find(".existing-reminder-at-alert")
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_reminder_alert_message(bookmark)
|
||||||
|
I18n.t(
|
||||||
|
"js.bookmarks.reminders.existing_reminder",
|
||||||
|
at_date_time:
|
||||||
|
I18n.t(
|
||||||
|
"js.bookmarks.reminders.at_time",
|
||||||
|
date_time:
|
||||||
|
bookmark
|
||||||
|
.reminder_at_in_zone(bookmark.user.user_option&.timezone || "UTC")
|
||||||
|
.strftime("%b %-d, %Y %l:%M %P")
|
||||||
|
.gsub(" ", " "), # have to do this because %l adds padding before the hour but not in JS
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user