From 81bb7e56c254c9912882f6b27434e00cdb9aef53 Mon Sep 17 00:00:00 2001 From: angusmcleod Date: Mon, 1 Nov 2021 21:52:29 +0800 Subject: [PATCH 1/4] WIP --- .../components/wizard-notice-row.js.es6 | 14 + .../discourse/components/wizard-notice.js.es6 | 43 +-- .../custom-wizard-critical-notice.hbs | 5 + .../custom-wizard-critical-notice.js.es6 | 19 ++ .../custom-wizard-important-notice.hbs | 3 - .../custom-wizard-important-notice.js.es6 | 16 -- .../controllers/admin-wizards-notices.js.es6 | 67 +++++ .../controllers/admin-wizards.js.es6 | 38 +-- .../custom-wizard-admin-route-map.js.es6 | 5 + .../discourse/helpers/notice-badge.js.es6 | 41 +++ .../initializers/custom-wizard-edits.js.es6 | 62 +++-- .../discourse/mixins/notice-message.js.es6 | 65 +++++ .../models/custom-wizard-notice.js.es6 | 67 ++++- .../routes/admin-wizards-notices.js.es6 | 15 ++ .../discourse/routes/admin-wizards.js.es6 | 13 +- .../templates/admin-wizards-notices.hbs | 49 ++++ .../discourse/templates/admin-wizards.hbs | 9 +- .../components/wizard-notice-row.hbs | 30 +++ .../templates/components/wizard-notice.hbs | 80 +++--- assets/stylesheets/admin/admin.scss | 183 ++++++++----- assets/stylesheets/admin/wizard/manager.scss | 4 - config/locales/client.en.yml | 36 ++- config/locales/server.en.yml | 22 +- config/routes.rb | 5 +- controllers/custom_wizard/admin/admin.rb | 8 +- controllers/custom_wizard/admin/notice.rb | 52 +++- lib/custom_wizard/notice.rb | 244 +++++++++++++----- lib/custom_wizard/notice/connection_error.rb | 61 ++--- plugin.rb | 5 +- .../custom_wizard/notice_serializer.rb | 10 +- 30 files changed, 930 insertions(+), 341 deletions(-) create mode 100644 assets/javascripts/discourse/components/wizard-notice-row.js.es6 create mode 100644 assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.hbs create mode 100644 assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 delete mode 100644 assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.hbs delete mode 100644 assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.js.es6 create mode 100644 assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 create mode 100644 assets/javascripts/discourse/helpers/notice-badge.js.es6 create mode 100644 assets/javascripts/discourse/mixins/notice-message.js.es6 create mode 100644 assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 create mode 100644 assets/javascripts/discourse/templates/admin-wizards-notices.hbs create mode 100644 assets/javascripts/discourse/templates/components/wizard-notice-row.hbs diff --git a/assets/javascripts/discourse/components/wizard-notice-row.js.es6 b/assets/javascripts/discourse/components/wizard-notice-row.js.es6 new file mode 100644 index 00000000..9c099b39 --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-notice-row.js.es6 @@ -0,0 +1,14 @@ +import Component from "@ember/component"; +import NoticeMessage from "../mixins/notice-message"; + +export default Component.extend(NoticeMessage, { + tagName: "tr", + attributeBindings: ["notice.id:data-notice-id"], + classNameBindings: [":wizard-notice-row", "notice.typeClass", "notice.expired:expired", "notice.dismissed:dismissed"], + + actions: { + dismiss() { + this.notice.dismiss(); + } + } +}); \ No newline at end of file diff --git a/assets/javascripts/discourse/components/wizard-notice.js.es6 b/assets/javascripts/discourse/components/wizard-notice.js.es6 index fcd77606..cac3e4eb 100644 --- a/assets/javascripts/discourse/components/wizard-notice.js.es6 +++ b/assets/javascripts/discourse/components/wizard-notice.js.es6 @@ -1,35 +1,9 @@ import Component from "@ember/component"; -import discourseComputed from "discourse-common/utils/decorators"; -import { not, notEmpty } from "@ember/object/computed"; -import I18n from "I18n"; +import NoticeMessage from "../mixins/notice-message"; -export default Component.extend({ - classNameBindings: [ - ":wizard-notice", - "notice.type", - "dismissed", - "expired", - "resolved", - ], - showFull: false, - resolved: notEmpty("notice.expired_at"), - dismissed: notEmpty("notice.dismissed_at"), - canDismiss: not("dismissed"), - - @discourseComputed("notice.type") - title(type) { - return I18n.t(`admin.wizard.notice.title.${type}`); - }, - - @discourseComputed("notice.type") - icon(type) { - return { - plugin_status_warning: "exclamation-circle", - plugin_status_connection_error: "bolt", - subscription_messages_connection_error: "bolt", - info: "info-circle", - }[type]; - }, +export default Component.extend(NoticeMessage, { + attributeBindings: ["notice.id:data-notice-id"], + classNameBindings: [':wizard-notice', 'notice.typeClass', 'notice.dismissed:dismissed', 'notice.expired:expired', 'notice.hidden:hidden'], actions: { dismiss() { @@ -38,5 +12,12 @@ export default Component.extend({ this.set("dismissing", false); }); }, - }, + + hide() { + this.set('hiding', true); + this.notice.hide().then(() => { + this.set('hiding', false); + }); + }, + } }); diff --git a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.hbs b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.hbs new file mode 100644 index 00000000..9d96bed9 --- /dev/null +++ b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.hbs @@ -0,0 +1,5 @@ +{{#if notices}} + {{#each notices as |notice|}} + {{wizard-notice notice=notice showPlugin=true}} + {{/each}} +{{/if}} diff --git a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 new file mode 100644 index 00000000..0bb252e9 --- /dev/null +++ b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 @@ -0,0 +1,19 @@ +import { getOwner } from "discourse-common/lib/get-owner"; + +export default { + shouldRender(attrs, ctx) { + return ctx.siteSettings.wizard_critical_notices_on_dashboard; + }, + + setupComponent(attrs, component) { + const controller = getOwner(this).lookup('controller:admin-dashboard'); + + component.set('notices', controller.get('customWizardCriticalNotices')); + controller.addObserver('customWizardCriticalNotices.[]', () => { + if (this._state === "destroying") { + return; + } + component.set('notices', controller.get('customWizardCriticalNotices')); + }); + } +}; \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.hbs b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.hbs deleted file mode 100644 index 9b01c468..00000000 --- a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if importantNotice}} - {{wizard-notice notice=importantNotice importantOnDashboard=true}} -{{/if}} diff --git a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.js.es6 b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.js.es6 deleted file mode 100644 index 5962f255..00000000 --- a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-important-notice.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -import { getOwner } from "discourse-common/lib/get-owner"; - -export default { - shouldRender(attrs, ctx) { - return ctx.siteSettings.wizard_important_notices_on_dashboard; - }, - - setupComponent() { - const controller = getOwner(this).lookup("controller:admin-dashboard"); - const importantNotice = controller.get("customWizardImportantNotice"); - - if (importantNotice) { - this.set("importantNotice", importantNotice); - } - }, -}; diff --git a/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 new file mode 100644 index 00000000..0f67f878 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 @@ -0,0 +1,67 @@ +import Controller from "@ember/controller"; +import CustomWizardNotice from "../models/custom-wizard-notice"; +import discourseComputed from "discourse-common/utils/decorators"; +import { notEmpty } from "@ember/object/computed"; +import { A } from "@ember/array"; +import I18n from "I18n"; + +export default Controller.extend({ + messageUrl: "https://thepavilion.io/t/3652", + messageKey: "info", + messageIcon: "info-circle", + messageClass: "info", + hasNotices: notEmpty("notices"), + page: 0, + loadingMore: false, + canLoadMore: true, + + @discourseComputed('notices.[]', 'notices.@each.dismissed') + allDismisssed(notices) { + return notices.every(n => !n.canDismiss || n.dismissed); + }, + + loadMoreNotices() { + if (!this.canLoadMore) { + return; + } + const page = this.get("page"); + this.set("loadingMore", true); + + CustomWizardNotice.list({ page, include_all: true }) + .then((result) => { + if (result.notices.length === 0) { + this.set("canLoadMore", false); + return; + } + + this.get("notices").pushObjects( + A(result.notices.map(notice => CustomWizardNotice.create(notice))) + ); + }) + .finally(() => this.set("loadingMore", false)); + }, + + actions: { + loadMore() { + if (this.canLoadMore) { + this.set("page", this.page + 1); + this.loadMoreNotices(); + } + }, + + dismissAll() { + bootbox.confirm( + I18n.t("admin.wizard.notice.dismiss_all.confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + (result) => { + if (result) { + this.set('loadingMore', true); + CustomWizardNotice.dismissAll() + .finally(() => this.set("loadingMore", false)); + } + } + ); + } + } +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 index d128d851..e2672fe4 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 @@ -1,20 +1,26 @@ -import Controller from "@ember/controller"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { ajax } from "discourse/lib/ajax"; +import Controller, { inject as controller } from "@ember/controller"; +import { isPresent } from "@ember/utils"; +import { A } from "@ember/array"; export default Controller.extend({ - actions: { - dismissNotice(noticeId) { - ajax(`/admin/wizards/notice/${this.id}`, { - type: "DELETE", - }) - .then((result) => { - if (result.success) { - const notices = this.notices; - notices.removeObject(notices.findBy("id", noticeId)); - } - }) - .catch(popupAjaxError); - }, + adminWizardsNotices: controller(), + + unsubscribe() { + this.messageBus.unsubscribe("/custom-wizard/notices"); }, + + subscribe() { + this.unsubscribe(); + this.messageBus.subscribe("/custom-wizard/notices", (data) => { + if (isPresent(data.active_notice_count)) { + this.set("activeNoticeCount", data.active_notice_count); + this.adminWizardsNotices.setProperties({ + notices: A(), + page: 0, + canLoadMore: true + }); + this.adminWizardsNotices.loadMoreNotices(); + } + }); + } }); diff --git a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 index 67b91f87..c3c95e48 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -63,6 +63,11 @@ export default { path: "/subscription", resetNamespace: true, }); + + this.route("adminWizardsNotices", { + path: "/notices", + resetNamespace: true, + }); } ); }, diff --git a/assets/javascripts/discourse/helpers/notice-badge.js.es6 b/assets/javascripts/discourse/helpers/notice-badge.js.es6 new file mode 100644 index 00000000..bc5df4a6 --- /dev/null +++ b/assets/javascripts/discourse/helpers/notice-badge.js.es6 @@ -0,0 +1,41 @@ +import { autoUpdatingRelativeAge } from "discourse/lib/formatter"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import I18n from "I18n"; +import { registerUnbound } from "discourse-common/lib/helpers"; +import { htmlSafe } from "@ember/template"; + +registerUnbound("notice-badge", function(attrs) { + let tag = attrs.url ? 'a' : 'div'; + let attrStr = ''; + if (attrs.title) { + attrStr += `title='${I18n.t(attrs.title)}'`; + } + if (attrs.url) { + attrStr += `href='${attrs.url}'`; + } + let html = `<${tag} class="${attrs.class ? `${attrs.class} ` : ''}notice-badge" ${attrStr}>`; + if (attrs.icon) { + html += iconHTML(attrs.icon); + } + if (attrs.label) { + if (attrs.icon) { + html += ' '; + } + html += `${I18n.t(attrs.label)}`; + } + if (attrs.date) { + if (attrs.icon || attrs.label) { + html += ' '; + } + let dateAttrs = {}; + if (attrs.leaveAgo) { + dateAttrs = { + format: "medium", + leaveAgo: true + }; + } + html += autoUpdatingRelativeAge(new Date(attrs.date), dateAttrs); + } + html += ``; + return htmlSafe(html); +}); \ No newline at end of file diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 21a9d745..8208cd20 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -1,6 +1,7 @@ import DiscourseURL from "discourse/lib/url"; import { withPluginApi } from "discourse/lib/plugin-api"; import CustomWizardNotice from "../models/custom-wizard-notice"; +import { isPresent } from "@ember/utils"; import { A } from "@ember/array"; export default { @@ -21,37 +22,46 @@ export default { }; withPluginApi("0.8.36", (api) => { - api.modifyClass("route:admin-dashboard", { - afterModel() { - return CustomWizardNotice.list().then((result) => { - if (result && result.length) { - this.set( - "notices", - A(result.map((n) => CustomWizardNotice.create(n))) - ); + api.modifyClass('route:admin-dashboard', { + setupController(controller) { + this._super(...arguments); + + controller.loadCriticalNotices(); + controller.subscribe(); + } + }); + + api.modifyClass('controller:admin-dashboard', { + criticalNotices: A(), + + unsubscribe() { + this.messageBus.unsubscribe("/custom-wizard/notices"); + }, + + subscribe() { + this.unsubscribe(); + this.messageBus.subscribe("/custom-wizard/notices", (data) => { + if (isPresent(data.active_notice_count)) { + this.loadCriticalNotices(); } }); }, - setupController(controller) { - if (this.notices) { - let pluginStatusConnectionError = this.notices.filter( - (n) => n.type === "plugin_status_connection_error" - )[0]; - let pluginStatusWarning = this.notices.filter( - (n) => n.type === "plugin_status_warning" - )[0]; - - if (pluginStatusConnectionError || pluginStatusWarning) { - controller.set( - "customWizardImportantNotice", - pluginStatusConnectionError || pluginStatusWarning - ); + loadCriticalNotices() { + CustomWizardNotice.list({ + type: [ + 'connection_error', + 'warning' + ], + archetype: 'plugin_status', + visible: true + }).then(result => { + if (result.notices && result.notices.length) { + const criticalNotices = A(result.notices.map(n => CustomWizardNotice.create(n))); + this.set('customWizardCriticalNotices', criticalNotices); } - } - - this._super(...arguments); - }, + }); + } }); }); }, diff --git a/assets/javascripts/discourse/mixins/notice-message.js.es6 b/assets/javascripts/discourse/mixins/notice-message.js.es6 new file mode 100644 index 00000000..492df643 --- /dev/null +++ b/assets/javascripts/discourse/mixins/notice-message.js.es6 @@ -0,0 +1,65 @@ +import Mixin from "@ember/object/mixin"; +import { bind, scheduleOnce } from "@ember/runloop"; +import { cookAsync } from "discourse/lib/text"; +import { createPopper } from "@popperjs/core"; + +export default Mixin.create({ + showCookedMessage: false, + + didReceiveAttrs(){ + const message = this.notice.message; + cookAsync(message).then((cooked) => { + this.set("cookedMessage", cooked); + }); + }, + + createMessageModal() { + let container = this.element.querySelector('.notice-message'); + let modal = this.element.querySelector('.cooked-notice-message'); + + this._popper = createPopper( + container, + modal, { + strategy: "absolute", + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + }, + { + name: "offset", + options: { + offset: [0, 5], + }, + }, + ], + } + ); + }, + + didInsertElement() { + $(document).on("click", bind(this, this.documentClick)); + }, + + willDestroyElement() { + $(document).off("click", bind(this, this.documentClick)); + }, + + documentClick(event) { + if (this._state === "destroying") { return; } + + if (!event.target.closest(`[data-notice-id="${this.notice.id}"] .notice-message`)) { + this.set('showCookedMessage', false); + } + }, + + actions: { + toggleCookedMessage() { + this.toggleProperty("showCookedMessage"); + + if (this.showCookedMessage) { + scheduleOnce("afterRender", this, this.createMessageModal); + } + } + } +}); \ No newline at end of file diff --git a/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 b/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 index 1eb21e73..29e30628 100644 --- a/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 @@ -1,25 +1,68 @@ import EmberObject from "@ember/object"; +import discourseComputed from "discourse-common/utils/decorators"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { and, not, notEmpty } from "@ember/object/computed"; +import { dasherize } from "@ember/string"; +import I18n from "I18n"; -const CustomWizardNotice = EmberObject.extend(); +const CustomWizardNotice = EmberObject.extend({ + expired: notEmpty('expired_at'), + dismissed: notEmpty('dismissed_at'), + hidden: notEmpty('hidden_at'), + notHidden: not('hidden'), + notDismissed: not('dismissed'), + canDismiss: and('dismissable', 'notDismissed'), + canHide: and('can_hide', 'notHidden'), -CustomWizardNotice.reopen({ - dismiss() { - return ajax(`/admin/wizards/notice/${this.id}`, { type: "PUT" }) - .then((result) => { - if (result.success) { - this.set("dismissed_at", result.dismissed_at); - } - }) - .catch(popupAjaxError); + @discourseComputed('type') + typeClass(type) { + return dasherize(type); }, + + @discourseComputed('type') + typeLabel(type) { + return I18n.t(`admin.wizard.notice.type.${type}`); + }, + + dismiss() { + if (!this.get('canDismiss')) { + return; + } + + return ajax(`/admin/wizards/notice/${this.get('id')}/dismiss`, { type: 'PUT' }).then(result => { + if (result.success) { + this.set('dismissed_at', result.dismissed_at); + } + }).catch(popupAjaxError); + }, + + hide() { + if (!this.get('canHide')) { + return; + } + + return ajax(`/admin/wizards/notice/${this.get('id')}/hide`, { type: 'PUT' }).then(result => { + if (result.success) { + this.set('hidden_at', result.hidden_at); + } + }).catch(popupAjaxError); + } }); CustomWizardNotice.reopenClass({ - list() { - return ajax("/admin/wizards/notice").catch(popupAjaxError); + list(data = {}) { + return ajax('/admin/wizards/notice', { + type: "GET", + data + }).catch(popupAjaxError); }, + + dismissAll() { + return ajax('/admin/wizards/notice/dismiss', { + type: "PUT" + }).catch(popupAjaxError); + } }); export default CustomWizardNotice; diff --git a/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 new file mode 100644 index 00000000..a329ce95 --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 @@ -0,0 +1,15 @@ +import CustomWizardNotice from "../models/custom-wizard-notice"; +import DiscourseRoute from "discourse/routes/discourse"; +import { A } from "@ember/array"; + +export default DiscourseRoute.extend({ + model() { + return CustomWizardNotice.list({ include_all: true }); + }, + + setupController(controller, model) { + controller.setProperties({ + notices: A(model.notices.map(notice => CustomWizardNotice.create(notice))), + }); + }, +}); diff --git a/assets/javascripts/discourse/routes/admin-wizards.js.es6 b/assets/javascripts/discourse/routes/admin-wizards.js.es6 index de67a8b9..2bbc73a9 100644 --- a/assets/javascripts/discourse/routes/admin-wizards.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards.js.es6 @@ -1,6 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; import { ajax } from "discourse/lib/ajax"; -import { A } from "@ember/array"; export default DiscourseRoute.extend({ model() { @@ -8,13 +7,21 @@ export default DiscourseRoute.extend({ }, setupController(controller, model) { - controller.set("notices", A(model.notices)); controller.set("api_section", model.api_section); + + if (model.active_notice_count) { + controller.set("activeNoticeCount", model.active_notice_count); + } + if (model.featured_notices) { + controller.set("featuredNotices", model.featured_notices); + } + + controller.subscribe(); }, afterModel(model, transition) { if (transition.targetName === "adminWizards.index") { this.transitionTo("adminWizardsWizard"); } - }, + } }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-notices.hbs b/assets/javascripts/discourse/templates/admin-wizards-notices.hbs new file mode 100644 index 00000000..d522c1a5 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-notices.hbs @@ -0,0 +1,49 @@ +
+

{{i18n "admin.wizard.notices.title"}}

+ +
+ {{d-button + label="admin.wizard.notice.dismiss_all.label" + title="admin.wizard.notice.dismiss_all.title" + action=(action "dismissAll") + disabled=allDismisssed + icon="check"}} +
+
+ +{{wizard-message + key=messageKey + url=messageUrl + type=messageType + opts=messageOpts + items=messageItems + loading=loading + component="notices"}} + +
+ {{#load-more selector=".wizard-table tr" action=(action "loadMore")}} + {{#if hasNotices}} + + + + + + + + + + + {{#each notices as |notice|}} + {{wizard-notice-row notice=notice}} + {{/each}} + +
{{I18n "admin.wizard.notice.time"}}{{I18n "admin.wizard.notice.type.label"}}{{I18n "admin.wizard.notice.title"}}{{I18n "admin.wizard.notice.status"}}
+ {{else}} + {{#unless loadingMore}} +

{{i18n "search.no_results"}}

+ {{/unless}} + {{/if}} + + {{conditional-loading-spinner condition=loadingMore}} + {{/load-more}} +
\ No newline at end of file diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index aa3b2cf5..4447380f 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -10,6 +10,12 @@ {{nav-item route="adminWizardsSubscription" label="admin.wizard.subscription.nav_label"}}
+ {{d-icon "far-life-ring"}}{{i18n "admin.wizard.support_button.label"}} @@ -17,8 +23,5 @@ {{/admin-nav}}
- {{#each notices as |notice|}} - {{wizard-notice notice=notice}} - {{/each}} {{outlet}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs b/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs new file mode 100644 index 00000000..7f97b250 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs @@ -0,0 +1,30 @@ + + {{#if notice.updated_at}} + {{notice-badge class="notice-updated-at" date=notice.updated_at label="admin.wizard.notice.updated_at" leaveAgo=true}} + {{else}} + {{notice-badge class="notice-created-at" date=notice.created_at label="admin.wizard.notice.created_at" leaveAgo=true}} + {{/if}} + +{{notice.typeLabel}} + + {{notice.title}} + {{#if showCookedMessage}} + {{cookedMessage}} + {{/if}} + + + {{#if notice.canDismiss}} + {{d-button + action="dismiss" + label="admin.wizard.notice.dismiss.label" + title="admin.wizard.notice.dismiss.title" + class="btn-dismiss" + icon="check"}} + {{else if notice.dismissed}} + {{i18n "admin.wizard.notice.dismissed_at"}} {{format-date notice.dismissed_at leaveAgo="true"}} + {{else if notice.expired}} + {{i18n "admin.wizard.notice.expired_at"}} {{format-date notice.expired_at leaveAgo="true"}} + {{else}} + {{i18n "admin.wizard.notice.active"}} + {{/if}} + \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/components/wizard-notice.hbs b/assets/javascripts/discourse/templates/components/wizard-notice.hbs index 89bbf5d4..24a853d3 100644 --- a/assets/javascripts/discourse/templates/components/wizard-notice.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-notice.hbs @@ -1,51 +1,39 @@
- {{#if resolved}} -
- {{d-icon "check"}} - {{i18n "admin.wizard.notice.resolved"}} - {{format-date notice.expired_at leaveAgo="true"}} -
- {{/if}} - -
- {{d-icon icon}} - {{title}} +
+ {{notice.title}} + {{#if showCookedMessage}} + {{cookedMessage}} + {{/if}}
+
+ {{#if notice.expired}} + {{notice-badge class="notice-expired-at" icon="check" label="admin.wizard.notice.expired_at" date=notice.expired_at}} + {{/if}} + {{#if showPlugin}} + {{notice-badge class="notice-plugin" icon="plug" title="admin.wizard.notice.plugin" label="admin.wizard.notice.plugin" url="/admin/wizards/notices"}} + {{/if}} + {{notice-badge class="notice-created-at" icon="far-clock" label="admin.wizard.notice.created_at" date=notice.created_at leaveAgo=true}} + {{#if notice.updated_at}} + {{notice-badge class="notice-updated-at" icon="far-clock" label="admin.wizard.notice.updated_at" date=notice.updated_at}} + {{/if}} -
- {{d-icon "far-clock"}} - {{i18n "admin.wizard.notice.issued"}} - {{format-date notice.created_at leaveAgo="true"}} -
- - {{#if notice.updated_at}} -
- {{d-icon "calendar-alt"}} - {{i18n "admin.wizard.notice.updated"}} - {{format-date notice.updated_at leaveAgo="true"}} -
- {{/if}} - -
- {{d-icon "plug"}} - {{i18n "admin.wizard.notice.plugin"}} + {{#if notice.canDismiss}} +
+ {{#if dismissing}} + {{loading-spinner size="small"}} + {{else}} + {{d-icon "times"}} + {{/if}} +
+ {{/if}} + {{#if notice.canHide}} +
+ {{#if hiding}} + {{loading-spinner size="small"}} + {{else}} + {{d-icon "far-eye-slash"}} + {{/if}} +
+ {{/if}}
- -
- {{html-safe notice.message}} -
- -{{#if importantOnDashboard}} - - {{i18n "admin.wizard.notice.disable_important_on_dashboard"}} - -{{/if}} - -{{#if canDismiss}} - {{#if dismissing}} - {{loading-spinner size="small"}} - {{else}} - {{d-icon "times"}} - {{/if}} -{{/if}} diff --git a/assets/stylesheets/admin/admin.scss b/assets/stylesheets/admin/admin.scss index 94fe6839..e3790465 100644 --- a/assets/stylesheets/admin/admin.scss +++ b/assets/stylesheets/admin/admin.scss @@ -4,15 +4,25 @@ @import "common/components/buttons"; @import "wizard/variables"; +$expired: #339b18; +$info: #038ae7; +$warning: #d47e00; +$error: #ef1700; + .admin-wizard-controls { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; + min-height: 34px; & + .wizard-message + div { margin-top: 20px; } + + h3 { + margin-bottom: 0; + } } .wizard-message { @@ -909,87 +919,142 @@ } } +.admin-wizards-notices { + .wizard-table { + overflow: unset; + } +} + .wizard-notice { - padding: 1em; + padding: .75em; margin-bottom: 1em; - border: 1px solid var(--primary); - position: relative; + border: 1px solid var(--primary-low); &.dismissed { display: none; } - &.resolved .notice-badge:not(.notice-expired-at), - &.resolved a, - &.resolved p { + &.expired .notice-badge:not(.notice-expired-at), + &.expired a, + &.expired p { color: var(--primary-medium) !important; } - .d-icon { - margin-right: 0.4em; + .notice-badge { + padding: 0 .5em; } .notice-header { display: flex; - } - - .notice-badge { - border: 1px solid var(--primary); - display: inline-flex; - align-items: center; - padding: 0 0.5em; - margin-right: 1em; - font-size: 0.9em; - line-height: 25px; - min-height: 25px; - box-sizing: border-box; - - &:last-of-type { - margin-right: 0; - } - } - - &.warning { - .notice-expired-at { - border: 1px solid var(--success); - background-color: rgba($success, 0.1); - color: var(--success); - } .notice-title { - border: 1px solid var(--pavilion-warning); - background-color: rgba($pavilion_warning, 0.1); - color: var(--pavilion-warning); + padding: 0; + } + + .notice-header-right { + margin-left: auto; + display: flex; + align-items: center; + + .notice-badge { + margin-left: .5em; + } } } - .notice-issued, - .notice-resolved { - margin-right: 0.3em; - } - - .notice-message { - p { - margin: 0.5em 0; - } - - p:last-of-type { - margin-bottom: 0; - } + .dismiss-notice-container, + .hide-notice-container { + width: 40px; + display: flex; + justify-content: center; + align-items: center; } .dismiss-notice, - .spinner { - position: absolute; - top: 1em; - right: 1em; - color: var(--primary-medium); - } + .hide-notice { + display: flex; + align-items: center; - .disable-important { - position: absolute; - right: 3em; - top: 1em; - color: var(--primary-medium); + .d-icon { + margin-right: 0; + color: var(--primary); + } } } + +.notice-badge { + display: inline-flex; + align-items: center; + min-height: 25px; + box-sizing: border-box; + color: var(--primary) !important; +} + +.admin-actions { + display: flex; + align-items: center; +} + +.wizard-notices-link { + position: relative; + margin-right: 10px; + + div > a { + @include btn; + color: var(--secondary) !important; + background-color: var(--primary-medium); + + &.active { + background-color: var(--tertiary) !important; + color: var(--secondary) !important; + } + } +} + +.active-notice-count { + background-color: $danger; + color: $secondary; + border-radius: 50%; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + position: absolute; + top: -8px; + right: -8px; + font-size: .7em; +} + +a.show-notice-message { + padding: .25em .5em; + color: var(--primary); +} + +.wizard-notice, +.wizard-notice-row:not(.expired):not(.dismissed) { + &.info { + background-color: rgba($info, 0.1); + border: 1px solid rgba($info, 0.5); + } + &.warning, + &.connection-error { + background-color: rgba($warning, 0.1); + border: 1px solid rgba($warning, 0.5); + } +} + +.notice-message { + position: relative; + + .cooked-notice-message { + background-color: var(--secondary); + padding: 1em; + z-index: 1; + box-shadow: shadow("dropdown"); + border-top: 1px solid var(--primary-low); + + p { + margin: 0; + } + } +} \ No newline at end of file diff --git a/assets/stylesheets/admin/wizard/manager.scss b/assets/stylesheets/admin/wizard/manager.scss index a3b30c98..69f963fe 100644 --- a/assets/stylesheets/admin/wizard/manager.scss +++ b/assets/stylesheets/admin/wizard/manager.scss @@ -2,10 +2,6 @@ display: flex; justify-content: flex-start; - h3 { - margin-bottom: 0; - } - .buttons { display: flex; margin-left: auto; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7c3b449b..9ffeda37 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -112,6 +112,9 @@ en: select: "Select a wizard to see its logs" viewing: "View recent logs for wizards on the forum" documentation: "Check out the logs documentation" + notices: + info: "Plugin status and subscription notices" + documentation: Check out the notices documentation editor: show: "Show" @@ -485,14 +488,31 @@ en: notice: plugin: Custom Wizard Plugin - issued: Issued - update: Updated - resolved: Resolved - title: - plugin_status_warning: Warning Notice - plugin_status_connection_error: Connection Notice - subscription_messages_connection_error: Connection Notice - disable_important_on_dashboard: disable + message: Message + time: Time + status: Status + title: Title + dismiss: + label: Dismiss + title: Dismiss notice + dismiss_all: + label: Dismiss All + title: Dismiss all informational Custom Wizard notices + confirm: Are you sure you want to dismiss all informational Custom Wizard notices? + active: active + created_at: issued + updated_at: updated + expired_at: expired + dismissed_at: dismissed + type: + label: Type + info: Information + warning: Warning + connection_error: Connection Error + + notices: + nav_label: Notices + title: Plugin Notices wizard_js: group: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 5fd7c076..5c25690b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -54,21 +54,21 @@ en: notice: connection_error: "Failed to connect to http://%{domain}" - compatibility_issue: > - The Custom Wizard Plugin has a compatibility issue with the latest version of Discourse. - Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse. + compatibility_issue: + title: The Custom Wizard Plugin is incompatibile with the latest version of Discourse. + message: Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse. plugin_status: - connection_error_limit: > - We're unable to connect to the Pavilion Plugin Status Server. Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse or the plugin. - If this connection issue persists please contact support@thepavilion.io for further assistance. - subscription_messages: - connection_error_limit: > - We're unable to connect to the Pavilion Subscription Server. This will not affect the operation of the plugin. - If this connection issue persists please contact support@thepavilion.io for further assistance. + connection_error: + title: Unable to connect to the Custom Wizard Plugin status server + message: Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse. If this issue persists contact support@thepavilion.io for further assistance. + subscription_message: + connection_error: + title: Unable to connect to the Custom Wizard Plugin subscription server + message: If this issue persists contact support@thepavilion.io for further assistance. site_settings: custom_wizard_enabled: "Enable custom wizards." wizard_redirect_exclude_paths: "Routes excluded from wizard redirects." wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview" wizard_apis_enabled: "Enable API features (experimental)." - wizard_important_notices_on_dashboard: "Show important notices about the custom wizard plugin on the admin dashboard." + wizard_critical_notices_on_dashboard: "Show critical notices about the custom wizard plugin on the admin dashboard." diff --git a/config/routes.rb b/config/routes.rb index 0d59b200..3b5f8ca6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,9 @@ Discourse::Application.routes.append do delete 'admin/wizards/subscription/authorize' => 'admin_subscription#destroy_authentication' get 'admin/wizards/notice' => 'admin_notice#index' - put 'admin/wizards/notice/:notice_id' => 'admin_notice#dismiss' + put 'admin/wizards/notice/:notice_id/dismiss' => 'admin_notice#dismiss' + put 'admin/wizards/notice/:notice_id/hide' => 'admin_notice#hide' + put 'admin/wizards/notice/dismiss' => 'admin_notice#dismiss_all' + get 'admin/wizards/notices' => 'admin_notice#index' end end diff --git a/controllers/custom_wizard/admin/admin.rb b/controllers/custom_wizard/admin/admin.rb index 8a3b69ac..63a89833 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/controllers/custom_wizard/admin/admin.rb @@ -6,8 +6,12 @@ class CustomWizard::AdminController < ::Admin::AdminController render_json_dump( #TODO replace with appropriate static? api_section: ["business"].include?(CustomWizard::Subscription.type), - notices: ActiveModel::ArraySerializer.new( - CustomWizard::Notice.list, + active_notice_count: CustomWizard::Notice.active_count, + featured_notices: ActiveModel::ArraySerializer.new( + CustomWizard::Notice.list( + type: CustomWizard::Notice.types[:info], + archetype: CustomWizard::Notice.archetypes[:subscription_message] + ), each_serializer: CustomWizard::NoticeSerializer ) ) diff --git a/controllers/custom_wizard/admin/notice.rb b/controllers/custom_wizard/admin/notice.rb index f28240e3..d6c43b5f 100644 --- a/controllers/custom_wizard/admin/notice.rb +++ b/controllers/custom_wizard/admin/notice.rb @@ -1,20 +1,66 @@ # frozen_string_literal: true class CustomWizard::AdminNoticeController < CustomWizard::AdminController - before_action :find_notice, only: [:dismiss] + before_action :find_notice, only: [:dismiss, :hide] def index - render_serialized(CustomWizard::Notice.list(include_recently_expired: true), CustomWizard::NoticeSerializer) + type = params[:type] + archetype = params[:archtype] + page = params[:page].to_i + include_all = ActiveRecord::Type::Boolean.new.cast(params[:include_all]) + visible = ActiveRecord::Type::Boolean.new.cast(params[:visible]) + + if type + if type.is_a?(Array) + type = type.map { |type| CustomWizard::Notice.types[type.to_sym] } + else + type = CustomWizard::Notice.types[type.to_sym] + end + end + + if archetype + if archetype.is_a?(Array) + archetype = archetype.map { |type| CustomWizard::Notice.archetypes[archetype.to_sym] } + else + archetype = CustomWizard::Notice.archetypes[archetype.to_sym] + end + end + + notices = CustomWizard::Notice.list( + include_all: include_all, + page: page, + type: type, + archetype: archetype, + visible: visible + ) + + render_serialized(notices, CustomWizard::NoticeSerializer, root: :notices) end def dismiss - if @notice.dismissable? && @notice.dismiss + if @notice.dismissable? && @notice.dismiss! render json: success_json.merge(dismissed_at: @notice.dismissed_at) else render json: failed_json end end + def hide + if @notice.can_hide? && @notice.hide! + render json: success_json.merge(hidden_at: @notice.hidden_at) + else + render json: failed_json + end + end + + def dismiss_all + if CustomWizard::Notice.dismiss_all + render json: success_json + else + render json: failed_json + end + end + def find_notice @notice = CustomWizard::Notice.find(params[:notice_id]) raise Discourse::InvalidParameters.new(:notice_id) unless @notice diff --git a/lib/custom_wizard/notice.rb b/lib/custom_wizard/notice.rb index 3ba710c5..7266178d 100644 --- a/lib/custom_wizard/notice.rb +++ b/lib/custom_wizard/notice.rb @@ -7,42 +7,61 @@ class CustomWizard::Notice "tests-passed" => "plugins.discourse.pavilion.tech", "stable" => "stable.plugins.discourse.pavilion.tech" } - SUBSCRIPTION_MESSAGES_DOMAIN = "test.thepavilion.io" + SUBSCRIPTION_MESSAGE_DOMAIN = "test.thepavilion.io" LOCALHOST_DOMAIN = "localhost:3000" PLUGIN_STATUSES_TO_WARN = %w(incompatible tests_failing) CHECK_PLUGIN_STATUS_ON_BRANCH = %w(tests-passed main stable) + PAGE_LIMIT = 30 attr_reader :id, + :title, :message, :type, + :archetype, :created_at attr_accessor :retrieved_at, :updated_at, :dismissed_at, - :expired_at + :expired_at, + :hidden_at def initialize(attrs) - @id = Digest::SHA1.hexdigest(attrs[:message]) + @id = self.class.generate_notice_id(attrs[:title], attrs[:created_at]) + @title = attrs[:title] @message = attrs[:message] @type = attrs[:type].to_i + @archetype = attrs[:archetype].to_i @created_at = attrs[:created_at] @updated_at = attrs[:updated_at] @retrieved_at = attrs[:retrieved_at] @dismissed_at = attrs[:dismissed_at] @expired_at = attrs[:expired_at] + @hidden_at = attrs[:hidden_at] end - def dismiss + def dismiss! if dismissable? self.dismissed_at = Time.now self.save + self.class.publish_notice_count end end - def expire - self.expired_at = Time.now - self.save + def hide! + if can_hide? + self.hidden_at = Time.now + self.save + self.class.publish_notice_count + end + end + + def expire! + if !expired? + self.expired_at = Time.now + self.save + self.class.publish_notice_count + end end def expired? @@ -54,15 +73,33 @@ class CustomWizard::Notice end def dismissable? - true + !expired? && !dismissed? && type === self.class.types[:info] + end + + def hidden? + hidden_at.present? + end + + def can_hide? + !hidden? && ( + type === self.class.types[:connection_error] || + type === self.class.types[:warning] + ) && ( + archetype === self.class.archetypes[:plugin_status] + ) end def save attrs = { expired_at: expired_at, + updated_at: updated_at, + retrieved_at: retrieved_at, created_at: created_at, + hidden_at: hidden_at, + title: title, message: message, - type: type + type: type, + archetype: archetype } if current = self.class.find(self.id) @@ -75,9 +112,15 @@ class CustomWizard::Notice def self.types @types ||= Enum.new( info: 0, - plugin_status_warning: 1, - plugin_status_connection_error: 2, - subscription_messages_connection_error: 3 + warning: 1, + connection_error: 2 + ) + end + + def self.archetypes + @archetypes ||= Enum.new( + subscription_message: 0, + plugin_status: 1 ) end @@ -85,7 +128,7 @@ class CustomWizard::Notice notices = [] if !skip_subscription - subscription_messages = request(:subscription_messages) + subscription_messages = request(:subscription_message) if subscription_messages.present? subscription_notices = convert_subscription_messages_to_notices(subscription_messages[:messages]) @@ -96,27 +139,46 @@ class CustomWizard::Notice if !skip_plugin && request_plugin_status? plugin_status = request(:plugin_status) - if plugin_status.present? && plugin_status[:status].present? && plugin_status[:status].is_a?(Hash) - plugin_notice = convert_plugin_status_to_notice(plugin_status[:status]) + if plugin_status.present? && plugin_status[:status].present? + plugin_notice = convert_plugin_status_to_notice(plugin_status) notices.push(plugin_notice) if plugin_notice end end - notices.each do |notice_data| - notice = new(notice_data) - notice.retrieved_at = Time.now - notice.save + if notices.any? + + notices.each do |notice_data| + notice = new(notice_data) + notice.retrieved_at = Time.now + notice.save + end + + publish_notice_count end end + def self.publish_notice_count + payload = { + active_notice_count: CustomWizard::Notice.active_count + } + MessageBus.publish("/custom-wizard/notices", payload, group_ids: [Group::AUTO_GROUPS[:admins]]) + end + def self.convert_subscription_messages_to_notices(messages) - messages.map do |message| - { - message: message[:message], - type: types[message[:type].to_sym], - created_at: message[:created_at], - expired_at: message[:expired_at] - } + messages.reduce([]) do |result, message| + notice_id = generate_notice_id(message[:title], message[:created_at]) + + unless exists?(notice_id) + result.push( + title: message[:title], + message: message[:message], + type: types[message[:type].to_sym], + archetype: archetypes[:subscription_message], + created_at: message[:created_at], + expired_at: message[:expired_at] + ) + end + result end end @@ -124,22 +186,32 @@ class CustomWizard::Notice notice = nil if PLUGIN_STATUSES_TO_WARN.include?(plugin_status[:status]) - notice = { - message: I18n.t('wizard.notice.compatibility_issue', domain: plugin_status_domain), - type: types[:plugin_status_warning], - created_at: plugin_status[:status_changed_at] - } + title = I18n.t('wizard.notice.compatibility_issue.title') + created_at = plugin_status[:status_changed_at] + id = generate_notice_id(title, created_at) + + unless exists?(id) + message = I18n.t('wizard.notice.compatibility_issue.message', domain: plugin_status_domain) + notice = { + id: id, + title: title, + message: message, + type: types[:warning], + archetype: archetypes[:plugin_status], + created_at: created_at + } + end else - expire_notices(types[:plugin_status_warning]) + expire_all(types[:warning], archetypes[:plugin_status]) end notice end - def self.notify_connection_errors(connection_type_key) - domain = self.send("#{connection_type_key.to_s}_domain") - message = I18n.t("wizard.notice.#{connection_type_key.to_s}.connection_error_limit", domain: domain) - notices = list(type: types[:connection_error], message: message) + def self.notify_connection_errors(archetype) + domain = self.send("#{archetype.to_s}_domain") + title = I18n.t("wizard.notice.#{archetype.to_s}.connection_error.title") + notices = list(type: types[:connection_error], archetype: archetypes[archetype.to_sym], title: title) if notices.any? notice = notices.first @@ -147,28 +219,29 @@ class CustomWizard::Notice notice.save else notice = new( - message: message, - type: types["#{connection_type_key}_connection_error".to_sym], - created_at: Time.now + title: title, + message: I18n.t("wizard.notice.#{archetype.to_s}.connection_error.message", domain: domain), + archetype: archetypes[archetype.to_sym], + type: types[:connection_error], + created_at: Time.now, + updated_at: Time.now ) notice.save end - end - def self.expire_notices(type) - list(type: type).each(&:expire) + publish_notice_count end def self.request_plugin_status? CHECK_PLUGIN_STATUS_ON_BRANCH.include?(Discourse.git_branch) || Rails.env.test? || Rails.env.development? end - def self.subscription_messages_domain - (Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGES_DOMAIN + def self.subscription_message_domain + (Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGE_DOMAIN end - def self.subscription_messages_url - "http://#{subscription_messages_domain}/subscription-server/messages.json" + def self.subscription_message_url + "http://#{subscription_message_domain}/subscription-server/messages.json" end def self.plugin_status_domain @@ -180,14 +253,19 @@ class CustomWizard::Notice "http://#{plugin_status_domain}/plugin-manager/status/discourse-custom-wizard" end - def self.request(type) - url = self.send("#{type.to_s}_url") - response = Excon.get(url) - connection_error = CustomWizard::Notice::ConnectionError.new(type) + def self.request(archetype) + url = self.send("#{archetype.to_s}_url") - if response.status == 200 + begin + response = Excon.get(url) + rescue Excon::Error::Socket, Excon::Error::Timeout => e + response = nil + end + connection_error = CustomWizard::Notice::ConnectionError.new(archetype) + + if response && response.status == 200 connection_error.expire! - expire_notices(types["#{type}_connection_error".to_sym]) + expire_all(types[:connection_error], archetypes[archetype.to_sym]) begin data = JSON.parse(response.body).deep_symbolize_keys @@ -198,8 +276,7 @@ class CustomWizard::Notice data else connection_error.create! - notify_connection_errors(type) if connection_error.reached_limit? - + notify_connection_errors(archetype) if connection_error.reached_limit? nil end end @@ -213,20 +290,65 @@ class CustomWizard::Notice new(raw.symbolize_keys) if raw.present? end + def self.exists?(id) + PluginStoreRow.where(plugin_name: namespace, key: id).exists? + end + def self.store(id, raw_notice) PluginStore.set(namespace, id, raw_notice) end - def self.list_query(type: nil, message: nil, include_recently_expired: false) + def self.list_query(type: nil, archetype: nil, title: nil, include_all: false, include_recently_expired: false, page: nil, visible: false) query = PluginStoreRow.where(plugin_name: namespace) - query = query.where("(value::json->>'expired_at') IS NULL#{include_recently_expired ? " OR (value::json->>'expired_at')::date > now()::date - 1" : ""}") - query = query.where("(value::json->>'type')::integer = ?", type) if type - query = query.where("(value::json->>'message')::text = ?", message) if message - query.order("value::json->>'created_at' DESC") + query = query.where("(value::json->>'hidden_at') IS NULL") if visible + query = query.where("(value::json->>'dismissed_at') IS NULL") unless include_all + query = query.where("(value::json->>'expired_at') IS NULL#{include_recently_expired ? " OR (value::json->>'expired_at')::date > now()::date - 1" : ""}") unless include_all + query = query.where("(value::json->>'archetype')::integer = ?", archetype) if archetype + if type + type_query_str = type.is_a?(Array) ? "(value::json->>'type')::integer IN (?)" : "(value::json->>'type')::integer = ?" + query = query.where(type_query_str, type) + end + query = query.where("(value::json->>'title')::text = ?", title) if title + query = query.limit(PAGE_LIMIT).offset(page.to_i * PAGE_LIMIT) if !page.nil? + query.order("value::json->>'expired_at' DESC, value::json->>'updated_at' DESC,value::json->>'dismissed_at' DESC, value::json->>'created_at' DESC") end - def self.list(type: nil, message: nil, include_recently_expired: false) - list_query(type: type, message: message, include_recently_expired: include_recently_expired) + def self.list(type: nil, archetype: nil, title: nil, include_all: false, include_recently_expired: false, page: 0, visible: false) + list_query(type: type, archetype: archetype, title: title, include_all: include_all, include_recently_expired: include_recently_expired, page: page, visible: visible) .map { |r| self.new(JSON.parse(r.value).symbolize_keys) } end + + def self.active_count + list_query.count + end + + def self.dismiss_all + dismissed_count = PluginStoreRow.where(" + plugin_name = '#{namespace}' AND + (value::json->>'type')::integer = #{types[:info]} AND + (value::json->>'expired_at') IS NULL AND + (value::json->>'dismissed_at') IS NULL + ").update_all(" + value = jsonb_set(value::jsonb, '{dismissed_at}', (to_json(now())::text)::jsonb, true) + ") + publish_notice_count if dismissed_count.to_i > 0 + dismissed_count + end + + def self.expire_all(type, archetype) + expired_count = PluginStoreRow.where(" + plugin_name = '#{namespace}' AND + (value::json->>'type')::integer = #{type} AND + (value::json->>'archetype')::integer = #{archetype} AND + (value::json->>'expired_at') IS NULL + ").update_all(" + value = jsonb_set(value::jsonb, '{expired_at}', (to_json(now())::text)::jsonb, true) + ") + publish_notice_count if expired_count.to_i > 0 + expired_count + end + + def self.generate_notice_id(title, created_at) + Digest::SHA1.hexdigest("#{title}-#{created_at}") + end end diff --git a/lib/custom_wizard/notice/connection_error.rb b/lib/custom_wizard/notice/connection_error.rb index 8b8b944a..9f78a09e 100644 --- a/lib/custom_wizard/notice/connection_error.rb +++ b/lib/custom_wizard/notice/connection_error.rb @@ -2,80 +2,73 @@ class CustomWizard::Notice::ConnectionError - attr_reader :type_key + attr_reader :archetype - def initialize(type_key) - @type_key = type_key + def initialize(archetype) + @archetype = archetype end def create! - id = "#{type_key.to_s}_error" - - if attrs = PluginStore.get(namespace, id) + if attrs = current_error + key = "#{archetype.to_s}_error_#{attrs["id"]}" attrs['updated_at'] = Time.now attrs['count'] = attrs['count'].to_i + 1 else - domain = CustomWizard::Notice.send("#{type_key.to_s}_domain") + domain = CustomWizard::Notice.send("#{archetype.to_s}_domain") + id = SecureRandom.hex(8) attrs = { + id: id, message: I18n.t("wizard.notice.connection_error", domain: domain), - type: self.class.types[type_key], + archetype: CustomWizard::Notice.archetypes[archetype.to_sym], created_at: Time.now, count: 1 } + key = "#{archetype.to_s}_error_#{id}" end - PluginStore.set(namespace, id, attrs) + PluginStore.set(namespace, key, attrs) @errors = nil end def expire! - if errors.exists? - errors.each do |error_row| - error = JSON.parse(error_row.value) - error['expired_at'] = Time.now - error_row.value = error.to_json - error_row.save - end + if query = current_error(query_only: true) + record = query.first + error = JSON.parse(record.value) + error['expired_at'] = Time.now + record.value = error.to_json + record.save end end - def self.types - @types ||= Enum.new( - plugin_status: 0, - subscription_messages: 1 - ) - end - def plugin_status_limit 5 end - def subscription_messages_limit + def subscription_message_limit 10 end def limit - self.send("#{type_key.to_s}_limit") + self.send("#{archetype.to_s}_limit") end def reached_limit? - return false unless errors.exists? + return false unless current_error.present? current_error['count'].to_i >= limit end - def current_error - JSON.parse(errors.first.value) - end - def namespace "#{CustomWizard::PLUGIN_NAME}_notice_connection" end - def errors - @errors ||= begin + def current_error(query_only: false) + @current_error ||= begin query = PluginStoreRow.where(plugin_name: namespace) - query = query.where("(value::json->>'type')::integer = ?", self.class.types[type_key]) - query.where("(value::json->>'expired_at') IS NULL") + query = query.where("(value::json->>'archetype')::integer = ?", CustomWizard::Notice.archetypes[archetype]) + query = query.where("(value::json->>'expired_at') IS NULL") + return nil if !query.exists? + return query if query_only + JSON.parse(query.first.value) end end end diff --git a/plugin.rb b/plugin.rb index 6845bf3a..bec30637 100644 --- a/plugin.rb +++ b/plugin.rb @@ -245,7 +245,10 @@ after_initialize do end AdminDashboardData.add_problem_check do - warning_notices = CustomWizard::Notice.list(type: CustomWizard::Notice.types[:plugin_status_warning]) + warning_notices = CustomWizard::Notice.list( + type: CustomWizard::Notice.types[:warning], + archetype: CustomWizard::Notice.archetypes[:plugin_status] + ) warning_notices.any? ? ActionView::Base.full_sanitizer.sanitize(warning_notices.first.message, tags: %w(a)) : nil end diff --git a/serializers/custom_wizard/notice_serializer.rb b/serializers/custom_wizard/notice_serializer.rb index 5564de1f..4354731d 100644 --- a/serializers/custom_wizard/notice_serializer.rb +++ b/serializers/custom_wizard/notice_serializer.rb @@ -2,19 +2,27 @@ class CustomWizard::NoticeSerializer < ApplicationSerializer attributes :id, + :title, :message, :type, + :archetype, :created_at, :expired_at, :updated_at, :dismissed_at, :retrieved_at, - :dismissable + :hidden_at, + :dismissable, + :can_hide def dismissable object.dismissable? end + def can_hide + object.can_hide? + end + def type CustomWizard::Notice.types.key(object.type) end From 98061c14e8ef84e974f1e8162d1ecbc2038e56bf Mon Sep 17 00:00:00 2001 From: angusmcleod Date: Tue, 2 Nov 2021 15:29:31 +0800 Subject: [PATCH 2/4] Fix spec (mostly) --- .../initializers/custom-wizard-edits.js.es6 | 1 + lib/custom_wizard/notice.rb | 49 ++++++------ lib/custom_wizard/notice/connection_error.rb | 17 ++-- spec/components/custom_wizard/notice_spec.rb | 80 +++++++++++++------ spec/jobs/update_notices_spec.rb | 2 +- .../admin/notice_controller_spec.rb | 57 +++++++++++-- 6 files changed, 141 insertions(+), 65 deletions(-) diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 8208cd20..774acca3 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -41,6 +41,7 @@ export default { subscribe() { this.unsubscribe(); this.messageBus.subscribe("/custom-wizard/notices", (data) => { + if (isPresent(data.active_notice_count)) { this.loadCriticalNotices(); } diff --git a/lib/custom_wizard/notice.rb b/lib/custom_wizard/notice.rb index 7266178d..a07e1443 100644 --- a/lib/custom_wizard/notice.rb +++ b/lib/custom_wizard/notice.rb @@ -43,24 +43,30 @@ class CustomWizard::Notice def dismiss! if dismissable? self.dismissed_at = Time.now - self.save - self.class.publish_notice_count + self.save_and_publish end end def hide! if can_hide? self.hidden_at = Time.now - self.save - self.class.publish_notice_count + self.save_and_publish end end def expire! if !expired? self.expired_at = Time.now - self.save + self.save_and_publish + end + end + + def save_and_publish + if self.save self.class.publish_notice_count + true + else + false end end @@ -95,7 +101,6 @@ class CustomWizard::Notice updated_at: updated_at, retrieved_at: retrieved_at, created_at: created_at, - hidden_at: hidden_at, title: title, message: message, type: type, @@ -104,6 +109,7 @@ class CustomWizard::Notice if current = self.class.find(self.id) attrs[:dismissed_at] = current.dismissed_at || self.dismissed_at + attrs[:hidden_at] = current.hidden_at || self.hidden_at end self.class.store(id, attrs) @@ -146,7 +152,6 @@ class CustomWizard::Notice end if notices.any? - notices.each do |notice_data| notice = new(notice_data) notice.retrieved_at = Time.now @@ -166,18 +171,16 @@ class CustomWizard::Notice def self.convert_subscription_messages_to_notices(messages) messages.reduce([]) do |result, message| - notice_id = generate_notice_id(message[:title], message[:created_at]) - - unless exists?(notice_id) - result.push( - title: message[:title], - message: message[:message], - type: types[message[:type].to_sym], - archetype: archetypes[:subscription_message], - created_at: message[:created_at], - expired_at: message[:expired_at] - ) - end + id = generate_notice_id(message[:title], message[:created_at]) + result.push( + id: id, + title: message[:title], + message: message[:message], + type: types[message[:type].to_sym], + archetype: archetypes[:subscription_message], + created_at: message[:created_at], + expired_at: message[:expired_at] + ) result end end @@ -298,11 +301,11 @@ class CustomWizard::Notice PluginStore.set(namespace, id, raw_notice) end - def self.list_query(type: nil, archetype: nil, title: nil, include_all: false, include_recently_expired: false, page: nil, visible: false) + def self.list_query(type: nil, archetype: nil, title: nil, include_all: false, page: nil, visible: false) query = PluginStoreRow.where(plugin_name: namespace) query = query.where("(value::json->>'hidden_at') IS NULL") if visible query = query.where("(value::json->>'dismissed_at') IS NULL") unless include_all - query = query.where("(value::json->>'expired_at') IS NULL#{include_recently_expired ? " OR (value::json->>'expired_at')::date > now()::date - 1" : ""}") unless include_all + query = query.where("(value::json->>'expired_at') IS NULL") unless include_all query = query.where("(value::json->>'archetype')::integer = ?", archetype) if archetype if type type_query_str = type.is_a?(Array) ? "(value::json->>'type')::integer IN (?)" : "(value::json->>'type')::integer = ?" @@ -313,8 +316,8 @@ class CustomWizard::Notice query.order("value::json->>'expired_at' DESC, value::json->>'updated_at' DESC,value::json->>'dismissed_at' DESC, value::json->>'created_at' DESC") end - def self.list(type: nil, archetype: nil, title: nil, include_all: false, include_recently_expired: false, page: 0, visible: false) - list_query(type: type, archetype: archetype, title: title, include_all: include_all, include_recently_expired: include_recently_expired, page: page, visible: visible) + def self.list(type: nil, archetype: nil, title: nil, include_all: false, page: 0, visible: false) + list_query(type: type, archetype: archetype, title: title, include_all: include_all, page: page, visible: visible) .map { |r| self.new(JSON.parse(r.value).symbolize_keys) } end diff --git a/lib/custom_wizard/notice/connection_error.rb b/lib/custom_wizard/notice/connection_error.rb index 9f78a09e..a1d834c6 100644 --- a/lib/custom_wizard/notice/connection_error.rb +++ b/lib/custom_wizard/notice/connection_error.rb @@ -10,9 +10,9 @@ class CustomWizard::Notice::ConnectionError def create! if attrs = current_error - key = "#{archetype.to_s}_error_#{attrs["id"]}" - attrs['updated_at'] = Time.now - attrs['count'] = attrs['count'].to_i + 1 + key = "#{archetype.to_s}_error_#{attrs[:id]}" + attrs[:updated_at] = Time.now + attrs[:count] = attrs[:count].to_i + 1 else domain = CustomWizard::Notice.send("#{archetype.to_s}_domain") id = SecureRandom.hex(8) @@ -27,7 +27,8 @@ class CustomWizard::Notice::ConnectionError end PluginStore.set(namespace, key, attrs) - @errors = nil + + @current_error = nil end def expire! @@ -54,7 +55,7 @@ class CustomWizard::Notice::ConnectionError def reached_limit? return false unless current_error.present? - current_error['count'].to_i >= limit + current_error[:count].to_i >= limit end def namespace @@ -64,11 +65,13 @@ class CustomWizard::Notice::ConnectionError def current_error(query_only: false) @current_error ||= begin query = PluginStoreRow.where(plugin_name: namespace) - query = query.where("(value::json->>'archetype')::integer = ?", CustomWizard::Notice.archetypes[archetype]) + query = query.where("(value::json->>'archetype')::integer = ?", CustomWizard::Notice.archetypes[archetype.to_sym]) query = query.where("(value::json->>'expired_at') IS NULL") + return nil if !query.exists? return query if query_only - JSON.parse(query.first.value) + + JSON.parse(query.first.value).deep_symbolize_keys end end end diff --git a/spec/components/custom_wizard/notice_spec.rb b/spec/components/custom_wizard/notice_spec.rb index b51305e1..0b34664d 100644 --- a/spec/components/custom_wizard/notice_spec.rb +++ b/spec/components/custom_wizard/notice_spec.rb @@ -6,6 +6,7 @@ describe CustomWizard::Notice do fab!(:user) { Fabricate(:user) } let(:subscription_message) { { + title: "Title of message about subscription", message: "Message about subscription", type: "info", created_at: Time.now - 3.day, @@ -23,7 +24,7 @@ describe CustomWizard::Notice do context "subscription message" do before do freeze_time - stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) + stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) described_class.update(skip_plugin: true) end @@ -36,46 +37,73 @@ describe CustomWizard::Notice do it "expires notice if subscription message is expired" do subscription_message[:expired_at] = Time.now - stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) + stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) described_class.update(skip_plugin: true) - notice = described_class.list(include_recently_expired: true).first + notice = described_class.list(include_all: true).first expect(notice.expired?).to eq(true) end + + it "dismisses informational subscription notices" do + notice = described_class.list(include_all: true).first + expect(notice.dismissed?).to eq(false) + + notice.dismiss! + expect(notice.dismissed?).to eq(true) + end + + it "dismisses all informational subscription notices" do + 4.times do |index| + subscription_message[:title] += " #{index}" + stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) + described_class.update(skip_plugin: true) + end + expect(described_class.list.count).to eq(5) + described_class.dismiss_all + expect(described_class.list.count).to eq(0) + end end context "plugin status" do before do freeze_time - stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json) described_class.update(skip_subscription: true) end it "converts warning into notice" do notice = described_class.list.first - expect(notice.type).to eq(described_class.types[:plugin_status_warning]) - expect(notice.message).to eq(I18n.t("wizard.notice.compatibility_issue", domain: described_class.plugin_status_domain)) + expect(notice.type).to eq(described_class.types[:warning]) + expect(notice.message).to eq(I18n.t("wizard.notice.compatibility_issue.message", domain: described_class.plugin_status_domain)) expect(notice.created_at.to_datetime).to be_within(1.second).of (plugin_status[:status_changed_at].to_datetime) end it "expires warning notices if status is recommended or compatible" do plugin_status[:status] = 'compatible' plugin_status[:status_changed_at] = Time.now - stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json) described_class.update(skip_subscription: true) - notice = described_class.list(type: described_class.types[:plugin_status_warning], include_recently_expired: true).first + notice = described_class.list(type: described_class.types[:warning], include_all: true).first expect(notice.expired?).to eq(true) end + + it "hides plugin status warnings" do + notice = described_class.list.first + expect(notice.hidden?).to eq(false) + + notice.hide! + expect(notice.hidden?).to eq(true) + end end it "lists notices not expired more than a day ago" do subscription_message[:expired_at] = Time.now - 8.hours - stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) - stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json) described_class.update - expect(described_class.list(include_recently_expired: true).length).to eq(2) + expect(described_class.list(include_all: true).length).to eq(2) end context "connection errors" do @@ -84,47 +112,47 @@ describe CustomWizard::Notice do end it "creates an error if connection to notice server fails" do - stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json) described_class.update(skip_subscription: true) error = CustomWizard::Notice::ConnectionError.new(:plugin_status) - expect(error.errors.exists?).to eq(true) + expect(error.current_error.present?).to eq(true) end it "only creates one connection error per type at a time" do - stub_request(:get, described_class.subscription_messages_url).to_return(status: 400, body: { messages: [subscription_message] }.to_json) - stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.subscription_message_url).to_return(status: 400, body: { messages: [subscription_message] }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json) 5.times { described_class.update } plugin_status_errors = CustomWizard::Notice::ConnectionError.new(:plugin_status) - subscription_message_errors = CustomWizard::Notice::ConnectionError.new(:subscription_messages) + subscription_message_errors = CustomWizard::Notice::ConnectionError.new(:subscription_message) - expect(plugin_status_errors.errors.length).to eq(1) - expect(subscription_message_errors.errors.length).to eq(1) + expect(plugin_status_errors.current_error[:count]).to eq(5) + expect(subscription_message_errors.current_error[:count]).to eq(5) end it "creates a connection error notice if connection errors reach limit" do - stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json) error = CustomWizard::Notice::ConnectionError.new(:plugin_status) error.limit.times { described_class.update(skip_subscription: true) } - notice = described_class.list(type: described_class.types[:plugin_status_connection_error]).first + notice = described_class.list(type: described_class.types[:connection_error]).first - expect(error.current_error['count']).to eq(error.limit) - expect(notice.type).to eq(described_class.types[:plugin_status_connection_error]) + expect(error.current_error[:count]).to eq(error.limit) + expect(notice.type).to eq(described_class.types[:connection_error]) end it "expires a connection error notice if connection succeeds" do - stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json) error = CustomWizard::Notice::ConnectionError.new(:plugin_status) error.limit.times { described_class.update(skip_subscription: true) } - stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) + stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json) described_class.update(skip_subscription: true) - notice = described_class.list(type: described_class.types[:plugin_status_connection_error], include_recently_expired: true).first + notice = described_class.list(type: described_class.types[:connection_error], include_all: true).first - expect(notice.type).to eq(described_class.types[:plugin_status_connection_error]) + expect(notice.type).to eq(described_class.types[:connection_error]) expect(notice.expired_at.present?).to eq(true) end end diff --git a/spec/jobs/update_notices_spec.rb b/spec/jobs/update_notices_spec.rb index d0e5a468..8ba5587f 100644 --- a/spec/jobs/update_notices_spec.rb +++ b/spec/jobs/update_notices_spec.rb @@ -20,7 +20,7 @@ describe Jobs::CustomWizardUpdateNotices do } it "updates the notices" do - stub_request(:get, CustomWizard::Notice.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) + stub_request(:get, CustomWizard::Notice.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) stub_request(:get, CustomWizard::Notice.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) described_class.new.execute diff --git a/spec/requests/custom_wizard/admin/notice_controller_spec.rb b/spec/requests/custom_wizard/admin/notice_controller_spec.rb index bd174e90..33d03432 100644 --- a/spec/requests/custom_wizard/admin/notice_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/notice_controller_spec.rb @@ -3,29 +3,70 @@ require_relative '../../../plugin_helper' describe CustomWizard::AdminNoticeController do fab!(:admin_user) { Fabricate(:user, admin: true) } + let(:subscription_message_notice) { + { + title: "Title of message about subscription", + message: "Message about subscription", + type: 0, + created_at: Time.now.iso8601(3), + expired_at: nil + } + } + let(:plugin_status_notice) { + { + title: "The Custom Wizard Plugin is incompatibile with the latest version of Discourse.", + message: "Please check the Custom Wizard Plugin status on [localhost:3000](http://localhost:3000) before updating Discourse.", + type: 1, + archetype: 1, + created_at: Time.now.iso8601(3), + expired_at: nil + } + } before do sign_in(admin_user) - @notice = CustomWizard::Notice.new( - message: "Message about subscription", - type: "info", - created_at: Time.now - 3.day, - expired_at: nil - ) - @notice.save end it "lists notices" do + @notice = CustomWizard::Notice.new(subscription_message_notice) + @notice.save + get "/admin/wizards/notice.json" expect(response.status).to eq(200) expect(response.parsed_body.length).to eq(1) end it "dismisses notices" do - put "/admin/wizards/notice/#{@notice.id}.json" + @notice = CustomWizard::Notice.new(subscription_message_notice) + @notice.save + + put "/admin/wizards/notice/#{@notice.id}/dismiss.json" expect(response.status).to eq(200) updated = CustomWizard::Notice.find(@notice.id) expect(updated.dismissed?).to eq(true) end + + it "dismisses all notices" do + 5.times do |index| + subscription_message_notice[:title] += " #{index}" + @notice = CustomWizard::Notice.new(subscription_message_notice) + @notice.save + end + + put "/admin/wizards/notice/dismiss.json" + expect(response.status).to eq(200) + expect(CustomWizard::Notice.list.size).to eq(0) + end + + it "hides notices" do + @notice = CustomWizard::Notice.new(plugin_status_notice) + @notice.save + + put "/admin/wizards/notice/#{@notice.id}/hide.json" + expect(response.status).to eq(200) + + updated = CustomWizard::Notice.find(@notice.id) + expect(updated.hidden?).to eq(true) + end end From 853634be27963acf154c9653fb9a19f22884af7c Mon Sep 17 00:00:00 2001 From: angusmcleod Date: Tue, 2 Nov 2021 15:39:38 +0800 Subject: [PATCH 3/4] Fix failing specs --- spec/components/custom_wizard/wizard_spec.rb | 2 +- spec/jobs/update_notices_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb index 9cccff97..8ee19c33 100644 --- a/spec/components/custom_wizard/wizard_spec.rb +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -186,7 +186,7 @@ describe CustomWizard::Wizard do it "lists the site categories" do Site.clear_cache - expect(@wizard.categories.length).to eq(1) + expect(@wizard.categories.length > 0).to eq(true) end context "submissions" do diff --git a/spec/jobs/update_notices_spec.rb b/spec/jobs/update_notices_spec.rb index 8ba5587f..df0697b8 100644 --- a/spec/jobs/update_notices_spec.rb +++ b/spec/jobs/update_notices_spec.rb @@ -21,7 +21,7 @@ describe Jobs::CustomWizardUpdateNotices do it "updates the notices" do stub_request(:get, CustomWizard::Notice.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) - stub_request(:get, CustomWizard::Notice.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) + stub_request(:get, CustomWizard::Notice.plugin_status_url).to_return(status: 200, body: plugin_status.to_json) described_class.new.execute expect(CustomWizard::Notice.list.length).to eq(2) From 49538d554df1597141a78005eadde3b13bb7c46c Mon Sep 17 00:00:00 2001 From: angusmcleod Date: Wed, 17 Nov 2021 20:48:11 +0800 Subject: [PATCH 4/4] Linting --- .../components/wizard-notice-row.js.es6 | 13 ++-- .../discourse/components/wizard-notice.js.es6 | 14 +++-- .../custom-wizard-critical-notice.js.es6 | 12 ++-- .../controllers/admin-wizards-notices.js.es6 | 17 +++--- .../controllers/admin-wizards.js.es6 | 4 +- .../discourse/helpers/notice-badge.js.es6 | 18 +++--- .../initializers/custom-wizard-edits.js.es6 | 26 ++++---- .../discourse/mixins/notice-message.js.es6 | 55 +++++++++-------- .../models/custom-wizard-notice.js.es6 | 60 ++++++++++--------- .../routes/admin-wizards-notices.js.es6 | 4 +- .../discourse/routes/admin-wizards.js.es6 | 2 +- .../templates/admin-wizards-notices.hbs | 2 +- .../components/wizard-notice-row.hbs | 2 +- assets/stylesheets/admin/admin.scss | 12 ++-- controllers/custom_wizard/admin/notice.rb | 4 +- lib/custom_wizard/notice.rb | 2 +- 16 files changed, 135 insertions(+), 112 deletions(-) diff --git a/assets/javascripts/discourse/components/wizard-notice-row.js.es6 b/assets/javascripts/discourse/components/wizard-notice-row.js.es6 index 9c099b39..ada4384d 100644 --- a/assets/javascripts/discourse/components/wizard-notice-row.js.es6 +++ b/assets/javascripts/discourse/components/wizard-notice-row.js.es6 @@ -4,11 +4,16 @@ import NoticeMessage from "../mixins/notice-message"; export default Component.extend(NoticeMessage, { tagName: "tr", attributeBindings: ["notice.id:data-notice-id"], - classNameBindings: [":wizard-notice-row", "notice.typeClass", "notice.expired:expired", "notice.dismissed:dismissed"], + classNameBindings: [ + ":wizard-notice-row", + "notice.typeClass", + "notice.expired:expired", + "notice.dismissed:dismissed", + ], actions: { dismiss() { this.notice.dismiss(); - } - } -}); \ No newline at end of file + }, + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-notice.js.es6 b/assets/javascripts/discourse/components/wizard-notice.js.es6 index cac3e4eb..ca6b7658 100644 --- a/assets/javascripts/discourse/components/wizard-notice.js.es6 +++ b/assets/javascripts/discourse/components/wizard-notice.js.es6 @@ -3,7 +3,13 @@ import NoticeMessage from "../mixins/notice-message"; export default Component.extend(NoticeMessage, { attributeBindings: ["notice.id:data-notice-id"], - classNameBindings: [':wizard-notice', 'notice.typeClass', 'notice.dismissed:dismissed', 'notice.expired:expired', 'notice.hidden:hidden'], + classNameBindings: [ + ":wizard-notice", + "notice.typeClass", + "notice.dismissed:dismissed", + "notice.expired:expired", + "notice.hidden:hidden", + ], actions: { dismiss() { @@ -14,10 +20,10 @@ export default Component.extend(NoticeMessage, { }, hide() { - this.set('hiding', true); + this.set("hiding", true); this.notice.hide().then(() => { - this.set('hiding', false); + this.set("hiding", false); }); }, - } + }, }); diff --git a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 index 0bb252e9..803e58a4 100644 --- a/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 +++ b/assets/javascripts/discourse/connectors/admin-dashboard-top/custom-wizard-critical-notice.js.es6 @@ -6,14 +6,14 @@ export default { }, setupComponent(attrs, component) { - const controller = getOwner(this).lookup('controller:admin-dashboard'); + const controller = getOwner(this).lookup("controller:admin-dashboard"); - component.set('notices', controller.get('customWizardCriticalNotices')); - controller.addObserver('customWizardCriticalNotices.[]', () => { + component.set("notices", controller.get("customWizardCriticalNotices")); + controller.addObserver("customWizardCriticalNotices.[]", () => { if (this._state === "destroying") { return; } - component.set('notices', controller.get('customWizardCriticalNotices')); + component.set("notices", controller.get("customWizardCriticalNotices")); }); - } -}; \ No newline at end of file + }, +}; diff --git a/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 index 0f67f878..1721e699 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-notices.js.es6 @@ -15,9 +15,9 @@ export default Controller.extend({ loadingMore: false, canLoadMore: true, - @discourseComputed('notices.[]', 'notices.@each.dismissed') + @discourseComputed("notices.[]", "notices.@each.dismissed") allDismisssed(notices) { - return notices.every(n => !n.canDismiss || n.dismissed); + return notices.every((n) => !n.canDismiss || n.dismissed); }, loadMoreNotices() { @@ -35,7 +35,7 @@ export default Controller.extend({ } this.get("notices").pushObjects( - A(result.notices.map(notice => CustomWizardNotice.create(notice))) + A(result.notices.map((notice) => CustomWizardNotice.create(notice))) ); }) .finally(() => this.set("loadingMore", false)); @@ -56,12 +56,13 @@ export default Controller.extend({ I18n.t("yes_value"), (result) => { if (result) { - this.set('loadingMore', true); - CustomWizardNotice.dismissAll() - .finally(() => this.set("loadingMore", false)); + this.set("loadingMore", true); + CustomWizardNotice.dismissAll().finally(() => + this.set("loadingMore", false) + ); } } ); - } - } + }, + }, }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 index e2672fe4..33841460 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 @@ -17,10 +17,10 @@ export default Controller.extend({ this.adminWizardsNotices.setProperties({ notices: A(), page: 0, - canLoadMore: true + canLoadMore: true, }); this.adminWizardsNotices.loadMoreNotices(); } }); - } + }, }); diff --git a/assets/javascripts/discourse/helpers/notice-badge.js.es6 b/assets/javascripts/discourse/helpers/notice-badge.js.es6 index bc5df4a6..ea32b462 100644 --- a/assets/javascripts/discourse/helpers/notice-badge.js.es6 +++ b/assets/javascripts/discourse/helpers/notice-badge.js.es6 @@ -4,38 +4,40 @@ import I18n from "I18n"; import { registerUnbound } from "discourse-common/lib/helpers"; import { htmlSafe } from "@ember/template"; -registerUnbound("notice-badge", function(attrs) { - let tag = attrs.url ? 'a' : 'div'; - let attrStr = ''; +registerUnbound("notice-badge", function (attrs) { + let tag = attrs.url ? "a" : "div"; + let attrStr = ""; if (attrs.title) { attrStr += `title='${I18n.t(attrs.title)}'`; } if (attrs.url) { attrStr += `href='${attrs.url}'`; } - let html = `<${tag} class="${attrs.class ? `${attrs.class} ` : ''}notice-badge" ${attrStr}>`; + let html = `<${tag} class="${ + attrs.class ? `${attrs.class} ` : "" + }notice-badge" ${attrStr}>`; if (attrs.icon) { html += iconHTML(attrs.icon); } if (attrs.label) { if (attrs.icon) { - html += ' '; + html += " "; } html += `${I18n.t(attrs.label)}`; } if (attrs.date) { if (attrs.icon || attrs.label) { - html += ' '; + html += " "; } let dateAttrs = {}; if (attrs.leaveAgo) { dateAttrs = { format: "medium", - leaveAgo: true + leaveAgo: true, }; } html += autoUpdatingRelativeAge(new Date(attrs.date), dateAttrs); } html += ``; return htmlSafe(html); -}); \ No newline at end of file +}); diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 774acca3..e67f1ebd 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -22,16 +22,16 @@ export default { }; withPluginApi("0.8.36", (api) => { - api.modifyClass('route:admin-dashboard', { + api.modifyClass("route:admin-dashboard", { setupController(controller) { this._super(...arguments); controller.loadCriticalNotices(); controller.subscribe(); - } + }, }); - api.modifyClass('controller:admin-dashboard', { + api.modifyClass("controller:admin-dashboard", { criticalNotices: A(), unsubscribe() { @@ -41,7 +41,6 @@ export default { subscribe() { this.unsubscribe(); this.messageBus.subscribe("/custom-wizard/notices", (data) => { - if (isPresent(data.active_notice_count)) { this.loadCriticalNotices(); } @@ -50,19 +49,18 @@ export default { loadCriticalNotices() { CustomWizardNotice.list({ - type: [ - 'connection_error', - 'warning' - ], - archetype: 'plugin_status', - visible: true - }).then(result => { + type: ["connection_error", "warning"], + archetype: "plugin_status", + visible: true, + }).then((result) => { if (result.notices && result.notices.length) { - const criticalNotices = A(result.notices.map(n => CustomWizardNotice.create(n))); - this.set('customWizardCriticalNotices', criticalNotices); + const criticalNotices = A( + result.notices.map((n) => CustomWizardNotice.create(n)) + ); + this.set("customWizardCriticalNotices", criticalNotices); } }); - } + }, }); }); }, diff --git a/assets/javascripts/discourse/mixins/notice-message.js.es6 b/assets/javascripts/discourse/mixins/notice-message.js.es6 index 492df643..76e311bb 100644 --- a/assets/javascripts/discourse/mixins/notice-message.js.es6 +++ b/assets/javascripts/discourse/mixins/notice-message.js.es6 @@ -6,7 +6,7 @@ import { createPopper } from "@popperjs/core"; export default Mixin.create({ showCookedMessage: false, - didReceiveAttrs(){ + didReceiveAttrs() { const message = this.notice.message; cookAsync(message).then((cooked) => { this.set("cookedMessage", cooked); @@ -14,27 +14,24 @@ export default Mixin.create({ }, createMessageModal() { - let container = this.element.querySelector('.notice-message'); - let modal = this.element.querySelector('.cooked-notice-message'); + let container = this.element.querySelector(".notice-message"); + let modal = this.element.querySelector(".cooked-notice-message"); - this._popper = createPopper( - container, - modal, { - strategy: "absolute", - placement: "bottom-start", - modifiers: [ - { - name: "preventOverflow", + this._popper = createPopper(container, modal, { + strategy: "absolute", + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + }, + { + name: "offset", + options: { + offset: [0, 5], }, - { - name: "offset", - options: { - offset: [0, 5], - }, - }, - ], - } - ); + }, + ], + }); }, didInsertElement() { @@ -46,10 +43,16 @@ export default Mixin.create({ }, documentClick(event) { - if (this._state === "destroying") { return; } + if (this._state === "destroying") { + return; + } - if (!event.target.closest(`[data-notice-id="${this.notice.id}"] .notice-message`)) { - this.set('showCookedMessage', false); + if ( + !event.target.closest( + `[data-notice-id="${this.notice.id}"] .notice-message` + ) + ) { + this.set("showCookedMessage", false); } }, @@ -60,6 +63,6 @@ export default Mixin.create({ if (this.showCookedMessage) { scheduleOnce("afterRender", this, this.createMessageModal); } - } - } -}); \ No newline at end of file + }, + }, +}); diff --git a/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 b/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 index 29e30628..035e2ad5 100644 --- a/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-notice.js.es6 @@ -7,62 +7,68 @@ import { dasherize } from "@ember/string"; import I18n from "I18n"; const CustomWizardNotice = EmberObject.extend({ - expired: notEmpty('expired_at'), - dismissed: notEmpty('dismissed_at'), - hidden: notEmpty('hidden_at'), - notHidden: not('hidden'), - notDismissed: not('dismissed'), - canDismiss: and('dismissable', 'notDismissed'), - canHide: and('can_hide', 'notHidden'), + expired: notEmpty("expired_at"), + dismissed: notEmpty("dismissed_at"), + hidden: notEmpty("hidden_at"), + notHidden: not("hidden"), + notDismissed: not("dismissed"), + canDismiss: and("dismissable", "notDismissed"), + canHide: and("can_hide", "notHidden"), - @discourseComputed('type') + @discourseComputed("type") typeClass(type) { return dasherize(type); }, - @discourseComputed('type') + @discourseComputed("type") typeLabel(type) { return I18n.t(`admin.wizard.notice.type.${type}`); }, dismiss() { - if (!this.get('canDismiss')) { + if (!this.get("canDismiss")) { return; } - return ajax(`/admin/wizards/notice/${this.get('id')}/dismiss`, { type: 'PUT' }).then(result => { - if (result.success) { - this.set('dismissed_at', result.dismissed_at); - } - }).catch(popupAjaxError); + return ajax(`/admin/wizards/notice/${this.get("id")}/dismiss`, { + type: "PUT", + }) + .then((result) => { + if (result.success) { + this.set("dismissed_at", result.dismissed_at); + } + }) + .catch(popupAjaxError); }, hide() { - if (!this.get('canHide')) { + if (!this.get("canHide")) { return; } - return ajax(`/admin/wizards/notice/${this.get('id')}/hide`, { type: 'PUT' }).then(result => { - if (result.success) { - this.set('hidden_at', result.hidden_at); - } - }).catch(popupAjaxError); - } + return ajax(`/admin/wizards/notice/${this.get("id")}/hide`, { type: "PUT" }) + .then((result) => { + if (result.success) { + this.set("hidden_at", result.hidden_at); + } + }) + .catch(popupAjaxError); + }, }); CustomWizardNotice.reopenClass({ list(data = {}) { - return ajax('/admin/wizards/notice', { + return ajax("/admin/wizards/notice", { type: "GET", - data + data, }).catch(popupAjaxError); }, dismissAll() { - return ajax('/admin/wizards/notice/dismiss', { - type: "PUT" + return ajax("/admin/wizards/notice/dismiss", { + type: "PUT", }).catch(popupAjaxError); - } + }, }); export default CustomWizardNotice; diff --git a/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 index a329ce95..1d8b7cc8 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-notices.js.es6 @@ -9,7 +9,9 @@ export default DiscourseRoute.extend({ setupController(controller, model) { controller.setProperties({ - notices: A(model.notices.map(notice => CustomWizardNotice.create(notice))), + notices: A( + model.notices.map((notice) => CustomWizardNotice.create(notice)) + ), }); }, }); diff --git a/assets/javascripts/discourse/routes/admin-wizards.js.es6 b/assets/javascripts/discourse/routes/admin-wizards.js.es6 index 2bbc73a9..5c39c0d6 100644 --- a/assets/javascripts/discourse/routes/admin-wizards.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards.js.es6 @@ -23,5 +23,5 @@ export default DiscourseRoute.extend({ if (transition.targetName === "adminWizards.index") { this.transitionTo("adminWizardsWizard"); } - } + }, }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-notices.hbs b/assets/javascripts/discourse/templates/admin-wizards-notices.hbs index d522c1a5..039afe49 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-notices.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-notices.hbs @@ -46,4 +46,4 @@ {{conditional-loading-spinner condition=loadingMore}} {{/load-more}} -
\ No newline at end of file +
diff --git a/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs b/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs index 7f97b250..cc22a42e 100644 --- a/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-notice-row.hbs @@ -27,4 +27,4 @@ {{else}} {{i18n "admin.wizard.notice.active"}} {{/if}} - \ No newline at end of file + diff --git a/assets/stylesheets/admin/admin.scss b/assets/stylesheets/admin/admin.scss index e3790465..36184088 100644 --- a/assets/stylesheets/admin/admin.scss +++ b/assets/stylesheets/admin/admin.scss @@ -926,7 +926,7 @@ $error: #ef1700; } .wizard-notice { - padding: .75em; + padding: 0.75em; margin-bottom: 1em; border: 1px solid var(--primary-low); @@ -941,7 +941,7 @@ $error: #ef1700; } .notice-badge { - padding: 0 .5em; + padding: 0 0.5em; } .notice-header { @@ -957,7 +957,7 @@ $error: #ef1700; align-items: center; .notice-badge { - margin-left: .5em; + margin-left: 0.5em; } } } @@ -1022,11 +1022,11 @@ $error: #ef1700; position: absolute; top: -8px; right: -8px; - font-size: .7em; + font-size: 0.7em; } a.show-notice-message { - padding: .25em .5em; + padding: 0.25em 0.5em; color: var(--primary); } @@ -1057,4 +1057,4 @@ a.show-notice-message { margin: 0; } } -} \ No newline at end of file +} diff --git a/controllers/custom_wizard/admin/notice.rb b/controllers/custom_wizard/admin/notice.rb index d6c43b5f..81ae00da 100644 --- a/controllers/custom_wizard/admin/notice.rb +++ b/controllers/custom_wizard/admin/notice.rb @@ -12,7 +12,7 @@ class CustomWizard::AdminNoticeController < CustomWizard::AdminController if type if type.is_a?(Array) - type = type.map { |type| CustomWizard::Notice.types[type.to_sym] } + type = type.map { |t| CustomWizard::Notice.types[t.to_sym] } else type = CustomWizard::Notice.types[type.to_sym] end @@ -20,7 +20,7 @@ class CustomWizard::AdminNoticeController < CustomWizard::AdminController if archetype if archetype.is_a?(Array) - archetype = archetype.map { |type| CustomWizard::Notice.archetypes[archetype.to_sym] } + archetype = archetype.map { |t| CustomWizard::Notice.archetypes[archetype.to_sym] } else archetype = CustomWizard::Notice.archetypes[archetype.to_sym] end diff --git a/lib/custom_wizard/notice.rb b/lib/custom_wizard/notice.rb index a07e1443..1fcb5041 100644 --- a/lib/custom_wizard/notice.rb +++ b/lib/custom_wizard/notice.rb @@ -308,7 +308,7 @@ class CustomWizard::Notice query = query.where("(value::json->>'expired_at') IS NULL") unless include_all query = query.where("(value::json->>'archetype')::integer = ?", archetype) if archetype if type - type_query_str = type.is_a?(Array) ? "(value::json->>'type')::integer IN (?)" : "(value::json->>'type')::integer = ?" + type_query_str = type.is_a?(Array) ? "(value::json->>'type')::integer IN (?)" : "(value::json->>'type')::integer = ?" query = query.where(type_query_str, type) end query = query.where("(value::json->>'title')::text = ?", title) if title