DEV: Plugin-api methods for user-notifications route customizations (#24873)

This commit is contained in:
Mark VanLandingham 2023-12-13 15:15:42 -06:00 committed by GitHub
parent f7aefffea7
commit c051bfc2fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 96 additions and 2 deletions

View File

@ -523,6 +523,7 @@ GEM
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
arm64-darwin-20 arm64-darwin-20
arm64-darwin-21
arm64-darwin-22 arm64-darwin-22
x86_64-darwin-18 x86_64-darwin-18
x86_64-darwin-19 x86_64-darwin-19

View File

@ -11,6 +11,11 @@ import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
const _beforeLoadMoreCallbacks = [];
export function addBeforeLoadMoreCallback(fn) {
_beforeLoadMoreCallbacks.push(fn);
}
export default class UserNotificationsController extends Controller { export default class UserNotificationsController extends Controller {
@service modal; @service modal;
@service appEvents; @service appEvents;
@ -102,6 +107,14 @@ export default class UserNotificationsController extends Controller {
@action @action
loadMore() { loadMore() {
if (
_beforeLoadMoreCallbacks.length &&
!_beforeLoadMoreCallbacks.some((fn) => fn(this))
) {
// Return early if any callbacks return false, short-circuiting the default loading more logic
return;
}
this.model.loadMore(); this.model.loadMore();
} }
} }

View File

@ -31,6 +31,7 @@ import { addUserMenuProfileTabItem } from "discourse/components/user-menu/profil
import { addDiscoveryQueryParam } from "discourse/controllers/discovery/list"; import { addDiscoveryQueryParam } from "discourse/controllers/discovery/list";
import { registerFullPageSearchType } from "discourse/controllers/full-page-search"; import { registerFullPageSearchType } from "discourse/controllers/full-page-search";
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic"; import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
import { addBeforeLoadMoreCallback as addBeforeLoadMoreNotificationsCallback } from "discourse/controllers/user-notifications";
import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages"; import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages";
import { import {
addExtraIconRenderer, addExtraIconRenderer,
@ -88,6 +89,7 @@ import {
addSaveableUserOptionField, addSaveableUserOptionField,
} from "discourse/models/user"; } from "discourse/models/user";
import { setNewCategoryDefaultColors } from "discourse/routes/new-category"; import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
import { setNotificationsLimit } from "discourse/routes/user-notifications";
import { addComposerSaveErrorCallback } from "discourse/services/composer"; import { addComposerSaveErrorCallback } from "discourse/services/composer";
import { import {
addToHeaderIcons, addToHeaderIcons,
@ -141,7 +143,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/. // using the format described at https://keepachangelog.com/en/1.0.0/.
export const PLUGIN_API_VERSION = "1.18.0"; export const PLUGIN_API_VERSION = "1.19.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode. // This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) { function canModify(klass, type, resolverName, changes) {
@ -819,6 +821,21 @@ class PluginApi {
registerCustomPostMessageCallback1(type, callback); registerCustomPostMessageCallback1(type, callback);
} }
/**
* Registers a callback that will be evaluated when infinite scrolling would cause
* more notifications to be loaded. This can be used to prevent loading more unless
* a specific condition is met.
*
* Example:
*
* api.addBeforeLoadMoreNotificationsCallback((controller) => {
* return controller.allowLoadMore;
* });
*/
addBeforeLoadMoreNotificationsCallback(fn) {
addBeforeLoadMoreNotificationsCallback(fn);
}
/** /**
* Changes a setting associated with a widget. For example, if * Changes a setting associated with a widget. For example, if
* you wanted small avatars in the post stream: * you wanted small avatars in the post stream:
@ -1773,6 +1790,18 @@ class PluginApi {
setNewCategoryDefaultColors(backgroundColor, textColor); setNewCategoryDefaultColors(backgroundColor, textColor);
} }
/**
* Change the number of notifications that are loaded at /my/notifications
*
* ```
* api.setNotificationsLimit(20)
* ```
*
**/
setNotificationsLimit(limit) {
setNotificationsLimit(limit);
}
/** /**
* Add a callback to modify search results before displaying them. * Add a callback to modify search results before displaying them.
* *

View File

@ -2,6 +2,13 @@ import ViewingActionType from "discourse/mixins/viewing-action-type";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
const DEFAULT_LIMIT = 60;
let limit = DEFAULT_LIMIT;
export function setNotificationsLimit(newLimit) {
limit = newLimit;
}
export default DiscourseRoute.extend(ViewingActionType, { export default DiscourseRoute.extend(ViewingActionType, {
controllerName: "user-notifications", controllerName: "user-notifications",
queryParams: { filter: { refreshModel: true } }, queryParams: { filter: { refreshModel: true } },
@ -16,6 +23,7 @@ export default DiscourseRoute.extend(ViewingActionType, {
return this.store.find("notification", { return this.store.find("notification", {
username, username,
filter: params.filter, filter: params.filter,
limit,
}); });
} }
}, },

View File

@ -27,6 +27,10 @@
<UserMenu::MenuItem @item={{item}} /> <UserMenu::MenuItem @item={{item}} />
{{/each}} {{/each}}
<ConditionalLoadingSpinner @condition={{this.loading}} /> <ConditionalLoadingSpinner @condition={{this.loading}} />
<PluginOutlet
@name="user-notifications-list-bottom"
@outletArgs={{hash controller=this}}
/>
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@ -7,6 +7,14 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.19.0] - 2023-12-13
### Added
- Added `setNotificationsLimit` function, which sets a new limit for how many notifications are loaded for the user notifications route
- Added `addBeforeLoadMoreNotificationsCallback` function, which takes a function as the argument. All added callbacks are evaluated before `loadMore` is triggered for user notifications. If any callback returns false, notifications will not be loaded.
## [1.18.0] - 2023-12-1 ## [1.18.0] - 2023-12-1
### Added ### Added

View File

@ -52,6 +52,10 @@ module PageObjects
self self
end end
def click_primary_navigation_item(name)
page.find(primary_navigation_selector(name)).click
end
private private
def primary_navigation_selector(name) def primary_navigation_selector(name)

View File

@ -27,6 +27,10 @@ module PageObjects
def has_no_notification?(notification) def has_no_notification?(notification)
page.has_no_css?(".notification a[href='#{notification.url}']") page.has_no_css?(".notification a[href='#{notification.url}']")
end end
def has_notification_count_of?(count)
page.has_css?(".notification", count: count)
end
end end
end end
end end

View File

@ -3,6 +3,7 @@
describe "User notifications", type: :system do describe "User notifications", type: :system do
fab!(:user) fab!(:user)
let(:user_notifications_page) { PageObjects::Pages::UserNotifications.new } let(:user_notifications_page) { PageObjects::Pages::UserNotifications.new }
let(:user_page) { PageObjects::Pages::User.new }
fab!(:read_notification) { Fabricate(:notification, user: user, read: true) } fab!(:read_notification) { Fabricate(:notification, user: user, read: true) }
fab!(:unread_notification) { Fabricate(:notification, user: user, read: false) } fab!(:unread_notification) { Fabricate(:notification, user: user, read: false) }
@ -10,7 +11,7 @@ describe "User notifications", type: :system do
before { sign_in(user) } before { sign_in(user) }
describe "filtering" do describe "filtering" do
it "saves custom picture and system assigned pictures" do it "correctly filters all / read / unread notifications" do
user_notifications_page.visit(user) user_notifications_page.visit(user)
user_notifications_page.filter_dropdown user_notifications_page.filter_dropdown
expect(user_notifications_page).to have_selected_filter_value("all") expect(user_notifications_page).to have_selected_filter_value("all")
@ -28,4 +29,26 @@ describe "User notifications", type: :system do
expect(user_notifications_page).to have_notification(unread_notification) expect(user_notifications_page).to have_notification(unread_notification)
end end
end end
describe "setNotificationLimit & addBeforeLoadMoreNotificationsCallback plugin-api functions" do
it "Allows blocking loading via callback and limit" do
user_page.visit(user)
page.execute_script <<~JS
require("discourse/lib/plugin-api").withPluginApi("1.19.0", (api) => {
api.setNotificationsLimit(1);
api.addBeforeLoadMoreNotificationsCallback(() => {
return false;
})
})
JS
user_page.click_primary_navigation_item("notifications")
# It is 1 here because we blocked infinite scrolling. Even though the limit is 1,
# without the callback, we would have 2 items here as it immediately fires another request.
expect(user_notifications_page).to have_notification_count_of(1)
end
end
end end