diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs
index 08a7009ef2c..cfcfdeb8b4f 100644
--- a/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs
+++ b/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs
@@ -1,19 +1,22 @@
+import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
-import { i18n } from "discourse-i18n";
const AdminConfigAreaEmptyList =
- {{i18n @emptyLabel}}
-
+ {{htmlSafe @emptyLabel}}
+
+ {{#if @ctaLabel}}
+
+ {{/if}}
;
diff --git a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs
index d94c4176b7d..66b851fcb38 100644
--- a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs
+++ b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs
@@ -3,10 +3,13 @@ import { tracked } from "@glimmer/tracking";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
+import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
+import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
import DashboardNewFeatureItem from "admin/components/dashboard-new-feature-item";
export default class DashboardNewFeatures extends Component {
@@ -14,34 +17,44 @@ export default class DashboardNewFeatures extends Component {
@tracked newFeatures = null;
@tracked groupedNewFeatures = null;
- @tracked isLoaded = false;
+ @tracked isLoading = true;
+
+ constructor() {
+ super(...arguments);
+ this.args.onCheckForFeatures(this.loadNewFeatures);
+ }
@bind
- loadNewFeatures() {
- ajax("/admin/whats-new.json")
- .then((json) => {
- const items = json.new_features.reduce((acc, feature) => {
- const key = moment(feature.released_at || feature.created_at).format(
- "YYYY-MM"
- );
- acc[key] = acc[key] || [];
- acc[key].push(feature);
- return acc;
- }, {});
+ async loadNewFeatures(opts = {}) {
+ opts.forceRefresh ||= false;
+ this.isLoading = true;
- this.groupedNewFeatures = Object.keys(items).map((date) => {
- return {
- date: moment
- .tz(date, this.currentUser.user_option.timezone)
- .format("MMMM YYYY"),
- features: items[date],
- };
- });
- this.isLoaded = true;
- })
- .finally(() => {
- this.isLoaded = true;
+ try {
+ const json = await ajax(
+ "/admin/whats-new.json?force_refresh=" + opts.forceRefresh
+ );
+ const items = json.new_features.reduce((acc, feature) => {
+ const key = moment(feature.released_at || feature.created_at).format(
+ "YYYY-MM"
+ );
+ acc[key] = acc[key] || [];
+ acc[key].push(feature);
+ return acc;
+ }, {});
+
+ this.groupedNewFeatures = Object.keys(items).map((date) => {
+ return {
+ date: moment
+ .tz(date, this.currentUser.user_option.timezone)
+ .format("MMMM YYYY"),
+ features: items[date],
+ };
});
+ } catch (err) {
+ popupAjaxError(err);
+ } finally {
+ this.isLoading = false;
+ }
}
@@ -49,7 +62,7 @@ export default class DashboardNewFeatures extends Component {
class="admin-config-area__primary-content"
{{didInsert this.loadNewFeatures}}
>
- {{#if this.groupedNewFeatures}}
+
{{#each this.groupedNewFeatures as |groupedFeatures|}}
<:content>
@@ -58,15 +71,17 @@ export default class DashboardNewFeatures extends Component {
{{/each}}
+ {{else}}
+
{{/each}}
- {{else if this.isLoaded}}
- {{htmlSafe
- (i18n
- "admin.dashboard.new_features.previous_announcements"
- url="https://meta.discourse.org/tags/c/announcements/67/release-notes"
- )
- }}
- {{/if}}
+
}
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js b/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js
new file mode 100644
index 00000000000..2f4a8c3daa7
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js
@@ -0,0 +1,14 @@
+import Controller from "@ember/controller";
+import { action } from "@ember/object";
+
+export default class AdminWhatsNewController extends Controller {
+ @action
+ checkForUpdates() {
+ this.checkFeaturesCallback?.({ forceRefresh: true });
+ }
+
+ @action
+ bindCheckFeatures(checkFeaturesCallback) {
+ this.checkFeaturesCallback = checkFeaturesCallback;
+ }
+}
diff --git a/app/assets/javascripts/admin/addon/templates/whats-new.hbs b/app/assets/javascripts/admin/addon/templates/whats-new.hbs
index 751967c07d7..854339cfc96 100644
--- a/app/assets/javascripts/admin/addon/templates/whats-new.hbs
+++ b/app/assets/javascripts/admin/addon/templates/whats-new.hbs
@@ -10,10 +10,16 @@
@label={{i18n "admin.dashboard.new_features.title"}}
/>
+ <:actions as |actions|>
+
+
\ No newline at end of file
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 1ce362d2498..2253b89abb7 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -31,7 +31,19 @@ class Admin::DashboardController < Admin::StaffController
end
def new_features
- new_features = DiscourseUpdates.new_features
+ force_refresh = params[:force_refresh] == "true"
+
+ if force_refresh
+ RateLimiter.new(
+ current_user,
+ "force-refresh-new-features",
+ 5,
+ 1.minute,
+ apply_limit_to_staff: true,
+ ).performed!
+ end
+
+ new_features = DiscourseUpdates.new_features(force_refresh:)
if current_user.admin? && most_recent = new_features&.first
DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent["created_at"])
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index eaef3cbaf38..5accd159f38 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -5115,6 +5115,7 @@ en:
new_features:
title: "What's new"
+ check_for_updates: "Check for updates"
dashboard:
title: "Dashboard"
last_updated: "Dashboard updated:"
diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb
index 3ddae519197..b280728740a 100644
--- a/lib/discourse_updates.rb
+++ b/lib/discourse_updates.rb
@@ -132,7 +132,8 @@ module DiscourseUpdates
end
def new_features_payload
- response = Excon.new(new_features_endpoint).request(expects: [200], method: :Get)
+ response =
+ Excon.new(new_features_endpoint).request(expects: [200], method: :Get, read_timeout: 5)
response.body
end
@@ -141,7 +142,9 @@ module DiscourseUpdates
Discourse.redis.set(new_features_key, payload)
end
- def new_features
+ def new_features(force_refresh: false)
+ update_new_features if force_refresh
+
entries =
begin
JSON.parse(Discourse.redis.get(new_features_key))
diff --git a/spec/lib/discourse_updates_spec.rb b/spec/lib/discourse_updates_spec.rb
index c53b362f461..3b5dd23adba 100644
--- a/spec/lib/discourse_updates_spec.rb
+++ b/spec/lib/discourse_updates_spec.rb
@@ -308,6 +308,14 @@ RSpec.describe DiscourseUpdates do
expect(result[0]["title"]).to eq("Bells")
expect(result[1]["title"]).to eq("Whistles")
end
+
+ it "correctly refetches features if force_refresh is used" do
+ DiscourseUpdates.expects(:update_new_features).once
+ result = DiscourseUpdates.new_features
+ expect(result.length).to eq(3)
+ result = DiscourseUpdates.new_features(force_refresh: true)
+ expect(result.length).to eq(3)
+ end
end
describe "#get_last_viewed_feature_date" do
diff --git a/spec/requests/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb
index 1847e41a7b4..42c7311ccc3 100644
--- a/spec/requests/admin/dashboard_controller_spec.rb
+++ b/spec/requests/admin/dashboard_controller_spec.rb
@@ -191,6 +191,38 @@ RSpec.describe Admin::DashboardController do
expect(json["has_unseen_features"]).to eq(true)
end
+ it "allows for forcing a refresh of new features, busting the cache" do
+ populate_new_features
+
+ get "/admin/whats-new.json"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+ expect(json["new_features"].length).to eq(2)
+
+ get "/admin/whats-new.json"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+ expect(json["new_features"].length).to eq(2)
+
+ DiscourseUpdates.stubs(:new_features_payload).returns(
+ [
+ {
+ "id" => "3",
+ "emoji" => "🚀",
+ "title" => "Space platform launched!",
+ "description" => "Now to make it to the next planet unscathed...",
+ "created_at" => 1.minute.ago,
+ },
+ ].to_json,
+ )
+
+ get "/admin/whats-new.json?force_refresh=true"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+ expect(json["new_features"].length).to eq(1)
+ expect(json["new_features"][0]["id"]).to eq("3")
+ end
+
it "passes unseen feature state" do
populate_new_features
DiscourseUpdates.mark_new_features_as_seen(admin.id)