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