diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7d81ec8da..129966e580 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -176,14 +176,14 @@ jobs: - name: Plugin System Tests if: matrix.build_type == 'system' && matrix.target == 'plugins' - run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system + run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system --format documentation --profile - name: Upload failed system test screenshots uses: actions/upload-artifact@v3 if: matrix.build_type == 'system' && failure() with: name: failed-system-test-screenshots - path: tmp/screenshots/*.png + path: tmp/capybara/*.png - name: Check Annotations if: matrix.build_type == 'annotations' diff --git a/Gemfile.lock b/Gemfile.lock index 21a03955f2..6a00754e7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,7 +97,6 @@ GEM xpath (~> 3.2) cbor (0.5.9.6) certified (1.0.0) - childprocess (4.1.0) chunky_png (1.4.0) coderay (1.1.3) colored2 (3.1.2) @@ -115,7 +114,7 @@ GEM debug_inspector (1.1.0) diff-lcs (1.5.0) diffy (3.4.2) - digest (3.1.0) + digest (3.1.1) discourse-fonts (0.0.9) discourse-seed-fu (2.3.12) activerecord (>= 3.1) @@ -128,7 +127,7 @@ GEM regexp_parser (~> 2.2) email_reply_trimmer (0.1.13) erubi (1.11.0) - excon (0.94.0) + excon (0.95.0) execjs (2.8.1) exifr (1.3.10) fabrication (2.30.0) @@ -168,7 +167,7 @@ GEM image_size (3.2.0) in_threads (1.6.0) jmespath (1.6.2) - json (2.6.2) + json (2.6.3) json-schema (3.0.0) addressable (>= 2.8) json_schemer (0.2.23) @@ -230,21 +229,21 @@ GEM net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.1.3) + net-protocol (0.2.1) timeout net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.9-aarch64-linux) + nokogiri (1.13.10-aarch64-linux) racc (~> 1.4) - nokogiri (1.13.9-arm64-darwin) + nokogiri (1.13.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-darwin) + nokogiri (1.13.10-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.9-x86_64-linux) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -300,11 +299,11 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (5.0.0) + public_suffix (5.0.1) puma (6.0.0) nio4r (~> 2.0) r2 (0.2.7) - racc (1.6.0) + racc (1.6.1) rack (2.2.4) rack-mini-profiler (3.0.0) rack (>= 1.2.0) @@ -342,7 +341,7 @@ GEM msgpack (>= 0.4.3) optimist (>= 3.0.0) rchardet (1.8.0) - redis (4.7.1) + redis (4.8.0) redis-namespace (1.9.0) redis (>= 4) regexp_parser (2.6.1) @@ -367,7 +366,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.0) + rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -386,7 +385,7 @@ GEM json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) rspec-core (>= 2.14) - rubocop (1.39.0) + rubocop (1.40.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -422,8 +421,7 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.6.1) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.7.1) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -451,10 +449,10 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.23) - test-prof (1.0.11) + test-prof (1.1.0) thor (1.2.1) tilt (2.0.11) - timeout (0.3.0) + timeout (0.3.1) tzinfo (2.0.5) concurrent-ruby (~> 1.0) uglifier (4.2.0) @@ -467,7 +465,7 @@ GEM kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.16.0) - uri (0.11.0) + uri (0.12.0) uri_template (0.7.0) version_gem (1.1.1) webdrivers (5.2.0) diff --git a/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js b/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js new file mode 100644 index 0000000000..0e0158e868 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-penalty-similar-users.js @@ -0,0 +1,29 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Component.extend({ + tagName: "", + + @discourseComputed("type") + penaltyField(penaltyType) { + if (penaltyType === "suspend") { + return "can_be_suspended"; + } else if (penaltyType === "silence") { + return "can_be_silenced"; + } + }, + + @action + selectUserId(userId, event) { + if (!this.selectedUserIds) { + return; + } + + if (event.target.checked) { + this.selectedUserIds.pushObject(userId); + } else { + this.selectedUserIds.removeObject(userId); + } + }, +}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js deleted file mode 100644 index 1c8e59fd27..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-event-chooser.js +++ /dev/null @@ -1,47 +0,0 @@ -import Component from "@ember/component"; -import I18n from "I18n"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; - -export default Component.extend({ - classNames: ["hook-event"], - typeName: alias("type.name"), - - @discourseComputed("typeName") - name(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.name`); - }, - - @discourseComputed("typeName") - details(typeName) { - return I18n.t(`admin.web_hooks.${typeName}_event.details`); - }, - - @discourseComputed("model.[]", "typeName") - eventTypeExists(eventTypes, typeName) { - return eventTypes.any((event) => event.name === typeName); - }, - - @discourseComputed("eventTypeExists") - enabled: { - get(eventTypeExists) { - return eventTypeExists; - }, - set(value, eventTypeExists) { - const type = this.type; - const model = this.model; - // add an association when not exists - if (value !== eventTypeExists) { - if (value) { - model.addObject(type); - } else { - model.removeObjects( - model.filter((eventType) => eventType.name === type.name) - ); - } - } - - return value; - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js deleted file mode 100644 index dd13384a2d..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-event.js +++ /dev/null @@ -1,110 +0,0 @@ -import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; -import Component from "@ember/component"; -import I18n from "I18n"; -import { ajax } from "discourse/lib/ajax"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { inject as service } from "@ember/service"; - -export default Component.extend({ - tagName: "li", - expandDetails: null, - expandDetailsRequestKey: "request", - expandDetailsResponseKey: "response", - dialog: service(), - - @discourseComputed("model.status") - statusColorClasses(status) { - if (!status) { - return ""; - } - - if (status >= 200 && status <= 299) { - return "text-successful"; - } else { - return "text-danger"; - } - }, - - @discourseComputed("model.created_at") - createdAt(createdAt) { - return moment(createdAt).format("YYYY-MM-DD HH:mm:ss"); - }, - - @discourseComputed("model.duration") - completion(duration) { - const seconds = Math.floor(duration / 10.0) / 100.0; - return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); - }, - - @discourseComputed("expandDetails") - expandRequestIcon(expandDetails) { - return expandDetails === this.expandDetailsRequestKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - @discourseComputed("expandDetails") - expandResponseIcon(expandDetails) { - return expandDetails === this.expandDetailsResponseKey - ? "ellipsis-h" - : "ellipsis-v"; - }, - - actions: { - redeliver() { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.events.redeliver_confirm"), - didConfirm: () => { - return ajax( - `/admin/api/web_hooks/${this.get( - "model.web_hook_id" - )}/events/${this.get("model.id")}/redeliver`, - { type: "POST" } - ) - .then((json) => { - this.set("model", json.web_hook_event); - }) - .catch(popupAjaxError); - }, - }); - }, - - toggleRequest() { - const expandDetailsKey = this.expandDetailsRequestKey; - - if (this.expandDetails !== expandDetailsKey) { - let headers = Object.assign( - { - "Request URL": this.get("model.request_url"), - "Request method": "POST", - }, - ensureJSON(this.get("model.headers")) - ); - this.setProperties({ - headers: plainJSON(headers), - body: prettyJSON(this.get("model.payload")), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.payload"), - }); - } else { - this.set("expandDetails", null); - } - }, - - toggleResponse() { - const expandDetailsKey = this.expandDetailsResponseKey; - - if (this.expandDetails !== expandDetailsKey) { - this.setProperties({ - headers: plainJSON(this.get("model.response_headers")), - body: this.get("model.response_body"), - expandDetails: expandDetailsKey, - bodyLabel: I18n.t("admin.web_hooks.events.body"), - }); - } else { - this.set("expandDetails", null); - } - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js b/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js deleted file mode 100644 index 880e91621c..0000000000 --- a/app/assets/javascripts/admin/addon/components/admin-web-hook-status.js +++ /dev/null @@ -1,39 +0,0 @@ -import Component from "@ember/component"; -import I18n from "I18n"; -import discourseComputed from "discourse-common/utils/decorators"; -import { iconHTML } from "discourse-common/lib/icon-library"; -import { htmlSafe } from "@ember/template"; - -export default Component.extend({ - classes: ["text-muted", "text-danger", "text-successful", "text-muted"], - icons: ["far-circle", "times-circle", "circle", "circle"], - circleIcon: null, - deliveryStatus: null, - - @discourseComputed("deliveryStatuses", "model.last_delivery_status") - status(deliveryStatuses, lastDeliveryStatus) { - return deliveryStatuses.find((s) => s.id === lastDeliveryStatus); - }, - - @discourseComputed("status.id", "icons") - icon(statusId, icons) { - return icons[statusId - 1]; - }, - - @discourseComputed("status.id", "classes") - class(statusId, classes) { - return classes[statusId - 1]; - }, - - didReceiveAttrs() { - this._super(...arguments); - this.set( - "circleIcon", - htmlSafe(iconHTML(this.icon, { class: this.class })) - ); - this.set( - "deliveryStatus", - I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`) - ); - }, -}); diff --git a/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs new file mode 100644 index 0000000000..df4b1b6f11 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.hbs @@ -0,0 +1,11 @@ + diff --git a/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js new file mode 100644 index 0000000000..cc818e92b9 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event-chooser.js @@ -0,0 +1,41 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; + +export default class WebhookEventChooser extends Component { + get name() { + return I18n.t(`admin.web_hooks.${this.args.type.name}_event.name`); + } + + get details() { + return I18n.t(`admin.web_hooks.${this.args.type.name}_event.details`); + } + + get eventTypeExists() { + return this.args.eventTypes.any( + (event) => event.name === this.args.type.name + ); + } + + get enabled() { + return this.eventTypeExists; + } + + set enabled(value) { + const eventTypes = this.args.eventTypes; + + // add an association when not exists + if (value === this.eventTypeExists) { + return value; + } + + if (value) { + eventTypes.addObject(this.args.type); + } else { + eventTypes.removeObjects( + eventTypes.filter((eventType) => eventType.name === this.args.type.name) + ); + } + + return value; + } +} diff --git a/app/assets/javascripts/admin/addon/components/webhook-event.hbs b/app/assets/javascripts/admin/addon/components/webhook-event.hbs new file mode 100644 index 0000000000..38cdacf79b --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-event.hbs @@ -0,0 +1,39 @@ +
{{this.headers}}
+
+ {{this.body}}
+ {{i18n "admin.web_hooks.events.none"}}
diff --git a/app/assets/javascripts/admin/addon/components/webhook-events.js b/app/assets/javascripts/admin/addon/components/webhook-events.js new file mode 100644 index 0000000000..5f887202e4 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-events.js @@ -0,0 +1,89 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; +import { gt, readOnly } from "@ember/object/computed"; +import { bind } from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default class WebhookEvents extends Component { + @service messageBus; + @service store; + + @tracked pingEnabled = true; + @tracked events = []; + @tracked incomingEventIds = []; + + @readOnly("incomingEventIds.length") incomingCount; + @gt("incomingCount", 0) hasIncoming; + + constructor() { + super(...arguments); + this.loadEvents(); + } + + async loadEvents() { + this.events = await this.store.findAll( + "web-hook-event", + this.args.webhookId + ); + } + + @bind + subscribe() { + const channel = `/web_hook_events/${this.args.webhookId}`; + this.messageBus.subscribe(channel, this._addIncoming); + } + + @bind + unsubscribe() { + this.messageBus.unsubscribe("/web_hook_events/*", this._addIncoming); + } + + @bind + _addIncoming(data) { + if (data.event_type === "ping") { + this.pingEnabled = true; + } + + if (!this.incomingEventIds.includes(data.web_hook_event_id)) { + this.incomingEventIds.pushObject(data.web_hook_event_id); + } + } + + @action + async showInserted(event) { + event?.preventDefault(); + + const path = `/admin/api/web_hooks/${this.args.webhookId}/events/bulk`; + const data = await ajax(path, { + data: { ids: this.incomingEventIds }, + }); + + const objects = data.map((webhookEvent) => + this.store.createRecord("web-hook-event", webhookEvent) + ); + this.events.unshiftObjects(objects); + this.incomingEventIds = []; + } + + @action + loadMore() { + this.events.loadMore(); + } + + @action + async ping() { + this.pingEnabled = false; + + try { + await ajax(`/admin/api/web_hooks/${this.args.webhookId}/ping`, { + type: "POST", + }); + } catch (error) { + this.pingEnabled = true; + popupAjaxError(error); + } + } +} diff --git a/app/assets/javascripts/admin/addon/components/webhook-status.hbs b/app/assets/javascripts/admin/addon/components/webhook-status.hbs new file mode 100644 index 0000000000..a4e3b5c7a5 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-status.hbs @@ -0,0 +1,2 @@ +{{d-icon this.iconName (hash class=this.iconClass)}} +{{this.deliveryStatus}} diff --git a/app/assets/javascripts/admin/addon/components/webhook-status.js b/app/assets/javascripts/admin/addon/components/webhook-status.js new file mode 100644 index 0000000000..258dca54cc --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/webhook-status.js @@ -0,0 +1,24 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; + +export default class WebhookStatus extends Component { + iconNames = ["far-circle", "times-circle", "circle", "circle"]; + iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"]; + + get status() { + const lastStatus = this.args.webhook.last_delivery_status; + return this.args.deliveryStatuses.find((s) => s.id === lastStatus); + } + + get deliveryStatus() { + return I18n.t(`admin.web_hooks.delivery_status.${this.status.name}`); + } + + get iconName() { + return this.iconNames[this.status.id - 1]; + } + + get iconClass() { + return this.iconClasses[this.status.id - 1]; + } +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js new file mode 100644 index 0000000000..f24dfef939 --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-edit.js @@ -0,0 +1,101 @@ +import Controller, { inject as controller } from "@ember/controller"; +import EmberObject, { action } from "@ember/object"; +import I18n from "I18n"; +import { alias } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; +import { isEmpty } from "@ember/utils"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; + +export default Controller.extend({ + adminWebHooks: controller(), + dialog: service(), + eventTypes: alias("adminWebHooks.eventTypes"), + defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), + contentTypes: alias("adminWebHooks.contentTypes"), + + @discourseComputed + showTagsFilter() { + return this.siteSettings.tagging_enabled; + }, + + @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") + savingStatus(isSaving, saved, saveButtonDisabled) { + if (isSaving) { + return I18n.t("saving"); + } else if (!saveButtonDisabled && saved) { + return I18n.t("saved"); + } + // Use side effect of validation to clear saved text + this.set("saved", false); + return ""; + }, + + @discourseComputed("model.isNew") + saveButtonText(isNew) { + return isNew + ? I18n.t("admin.web_hooks.create") + : I18n.t("admin.web_hooks.save"); + }, + + @discourseComputed("model.secret") + secretValidation(secret) { + if (!isEmpty(secret)) { + if (secret.includes(" ")) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_invalid"), + }); + } + + if (secret.length < 12) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.secret_too_short"), + }); + } + } + }, + + @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") + eventTypeValidation(isWildcard, eventTypes) { + if (!isWildcard && isEmpty(eventTypes)) { + return EmberObject.create({ + failed: true, + reason: I18n.t("admin.web_hooks.event_type_missing"), + }); + } + }, + + @discourseComputed( + "model.isSaving", + "secretValidation", + "eventTypeValidation", + "model.payload_url" + ) + saveButtonDisabled( + isSaving, + secretValidation, + eventTypeValidation, + payloadUrl + ) { + return isSaving + ? false + : secretValidation || eventTypeValidation || isEmpty(payloadUrl); + }, + + @action + async save() { + this.set("saved", false); + + try { + await this.model.save(); + + this.set("saved", true); + this.adminWebHooks.model.addObject(this.model); + this.transitionToRoute("adminWebHooks.show", this.model); + } catch (e) { + popupAjaxError(e); + } + }, +}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js new file mode 100644 index 0000000000..a1294cf09d --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-index.js @@ -0,0 +1,36 @@ +import Controller, { inject as controller } from "@ember/controller"; +import I18n from "I18n"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; +import { alias } from "@ember/object/computed"; + +export default Controller.extend({ + adminWebHooks: controller(), + dialog: service(), + contentTypes: alias("adminWebHooks.contentTypes"), + defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), + deliveryStatuses: alias("adminWebHooks.deliveryStatuses"), + eventTypes: alias("adminWebHooks.eventTypes"), + model: alias("adminWebHooks.model"), + + @action + destroy(webhook) { + return this.dialog.deleteConfirm({ + message: I18n.t("admin.web_hooks.delete_confirm"), + didConfirm: async () => { + try { + await webhook.destroyRecord(); + this.model.removeObject(webhook); + } catch (e) { + popupAjaxError(e); + } + }, + }); + }, + + @action + loadMore() { + this.model.loadMore(); + }, +}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js deleted file mode 100644 index 84b7650ff1..0000000000 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show-events.js +++ /dev/null @@ -1,83 +0,0 @@ -import Controller from "@ember/controller"; -import { ajax } from "discourse/lib/ajax"; -import { action } from "@ember/object"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Controller.extend({ - pingDisabled: false, - incomingCount: alias("incomingEventIds.length"), - - init() { - this._super(...arguments); - - this.incomingEventIds = []; - }, - - @discourseComputed("incomingCount") - hasIncoming(incomingCount) { - return incomingCount > 0; - }, - - subscribe() { - this.messageBus.subscribe( - `/web_hook_events/${this.get("model.extras.web_hook_id")}`, - (data) => { - if (data.event_type === "ping") { - this.set("pingDisabled", false); - } - this._addIncoming(data.web_hook_event_id); - } - ); - }, - - unsubscribe() { - this.messageBus.unsubscribe("/web_hook_events/*"); - }, - - _addIncoming(eventId) { - const incomingEventIds = this.incomingEventIds; - - if (!incomingEventIds.includes(eventId)) { - incomingEventIds.pushObject(eventId); - } - }, - - @action - showInserted(event) { - event?.preventDefault(); - const webHookId = this.get("model.extras.web_hook_id"); - - ajax(`/admin/api/web_hooks/${webHookId}/events/bulk`, { - type: "GET", - data: { ids: this.incomingEventIds }, - }).then((data) => { - const objects = data.map((webHookEvent) => - this.store.createRecord("web-hook-event", webHookEvent) - ); - this.model.unshiftObjects(objects); - this.set("incomingEventIds", []); - }); - }, - - actions: { - loadMore() { - this.model.loadMore(); - }, - - ping() { - this.set("pingDisabled", true); - - ajax( - `/admin/api/web_hooks/${this.get("model.extras.web_hook_id")}/ping`, - { - type: "POST", - } - ).catch((error) => { - this.set("pingDisabled", false); - popupAjaxError(error); - }); - }, - }, -}); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js index f1e8caadcf..38bef778fb 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks-show.js @@ -1,121 +1,32 @@ import Controller, { inject as controller } from "@ember/controller"; -import EmberObject from "@ember/object"; +import { action } from "@ember/object"; import I18n from "I18n"; -import { alias } from "@ember/object/computed"; -import discourseComputed from "discourse-common/utils/decorators"; -import { isEmpty } from "@ember/utils"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; export default Controller.extend({ adminWebHooks: controller(), dialog: service(), - eventTypes: alias("adminWebHooks.eventTypes"), - defaultEventTypes: alias("adminWebHooks.defaultEventTypes"), - contentTypes: alias("adminWebHooks.contentTypes"), + router: service(), - @discourseComputed - showTagsFilter() { - return this.siteSettings.tagging_enabled; + @action + edit() { + return this.router.transitionTo("adminWebHooks.edit", this.model); }, - @discourseComputed("model.isSaving", "saved", "saveButtonDisabled") - savingStatus(isSaving, saved, saveButtonDisabled) { - if (isSaving) { - return I18n.t("saving"); - } else if (!saveButtonDisabled && saved) { - return I18n.t("saved"); - } - // Use side effect of validation to clear saved text - this.set("saved", false); - return ""; - }, - - @discourseComputed("model.isNew") - saveButtonText(isNew) { - return isNew - ? I18n.t("admin.web_hooks.create") - : I18n.t("admin.web_hooks.save"); - }, - - @discourseComputed("model.secret") - secretValidation(secret) { - if (!isEmpty(secret)) { - if (secret.includes(" ")) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_invalid"), - }); - } - - if (secret.length < 12) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.secret_too_short"), - }); - } - } - }, - - @discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]") - eventTypeValidation(isWildcard, eventTypes) { - if (!isWildcard && isEmpty(eventTypes)) { - return EmberObject.create({ - failed: true, - reason: I18n.t("admin.web_hooks.event_type_missing"), - }); - } - }, - - @discourseComputed( - "model.isSaving", - "secretValidation", - "eventTypeValidation", - "model.payload_url" - ) - saveButtonDisabled( - isSaving, - secretValidation, - eventTypeValidation, - payloadUrl - ) { - return isSaving - ? false - : secretValidation || eventTypeValidation || isEmpty(payloadUrl); - }, - - actions: { - save() { - this.set("saved", false); - const model = this.model; - const isNew = model.get("isNew"); - - return model - .save() - .then(() => { - this.set("saved", true); - this.adminWebHooks.get("model").addObject(model); - - if (isNew) { - this.transitionToRoute("adminWebHooks.show", model.get("id")); - } - }) - .catch(popupAjaxError); - }, - - destroy() { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.delete_confirm"), - didConfirm: () => { - this.model - .destroyRecord() - .then(() => { - this.adminWebHooks.get("model").removeObject(this.model); - this.transitionToRoute("adminWebHooks"); - }) - .catch(popupAjaxError); - }, - }); - }, + @action + destroy() { + return this.dialog.deleteConfirm({ + message: I18n.t("admin.web_hooks.delete_confirm"), + didConfirm: async () => { + try { + await this.model.destroyRecord(); + this.adminWebHooks.model.removeObject(this.model); + this.transitionToRoute("adminWebHooks"); + } catch (e) { + popupAjaxError(e); + } + }, + }); }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js index 405f859413..fa4ba1ee7d 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-web-hooks.js @@ -1,29 +1,3 @@ import Controller from "@ember/controller"; -import I18n from "I18n"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { inject as service } from "@ember/service"; -import { action } from "@ember/object"; -export default Controller.extend({ - dialog: service(), - - @action - destroy(webhook) { - return this.dialog.yesNoConfirm({ - message: I18n.t("admin.web_hooks.delete_confirm"), - didConfirm: () => { - webhook - .destroyRecord() - .then(() => { - this.model.removeObject(webhook); - }) - .catch(popupAjaxError); - }, - }); - }, - - @action - loadMore() { - this.model.loadMore(); - }, -}); +export default Controller.extend({}); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js index 3d9248d36d..bdbcf2d220 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-merge-users-progress.js @@ -2,30 +2,33 @@ import Controller from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import messageBus from "message-bus-client"; +import { bind } from "discourse-common/utils/decorators"; export default Controller.extend(ModalFunctionality, { message: I18n.t("admin.user.merging_user"), onShow() { - messageBus.subscribe("/merge_user", (data) => { - if (data.merged) { - if (/^\/admin\/users\/list\//.test(location)) { - DiscourseURL.redirectTo(location); - } else { - DiscourseURL.redirectTo( - `/admin/users/${data.user.id}/${data.user.username}` - ); - } - } else if (data.message) { - this.set("message", data.message); - } else if (data.failed) { - this.set("message", I18n.t("admin.user.merge_failed")); - } - }); + this.messageBus.subscribe("/merge_user", this.onMessage); }, onClose() { - this.messageBus.unsubscribe("/merge_user"); + this.messageBus.unsubscribe("/merge_user", this.onMessage); + }, + + @bind + onMessage(data) { + if (data.merged) { + if (/^\/admin\/users\/list\//.test(location)) { + DiscourseURL.redirectTo(location); + } else { + DiscourseURL.redirectTo( + `/admin/users/${data.user.id}/${data.user.username}` + ); + } + } else if (data.message) { + this.set("message", data.message); + } else if (data.failed) { + this.set("message", I18n.t("admin.user.merge_failed")); + } }, }); diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js index 506598625d..077a086330 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-silence-user.js @@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, { onShow() { this.resetModal(); - this.setProperties({ silenceUntil: null, silencing: false }); + this.setProperties({ + silenceUntil: null, + silencing: false, + otherUserIds: [], + }); }, finishedSetup() { @@ -36,6 +40,7 @@ export default Controller.extend(PenaltyController, { post_id: this.postId, post_action: this.postAction, post_edit: this.postEdit, + other_user_ids: this.otherUserIds, }); }).finally(() => this.set("silencing", false)); }, diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js index c82a562808..c9f34d8957 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-suspend-user.js @@ -9,7 +9,11 @@ export default Controller.extend(PenaltyController, { onShow() { this.resetModal(); - this.setProperties({ suspendUntil: null, suspending: false }); + this.setProperties({ + suspendUntil: null, + suspending: false, + otherUserIds: [], + }); }, finishedSetup() { @@ -28,7 +32,6 @@ export default Controller.extend(PenaltyController, { } this.set("suspending", true); - this.penalize(() => { return this.user.suspend({ suspend_until: this.suspendUntil, @@ -37,6 +40,7 @@ export default Controller.extend(PenaltyController, { post_id: this.postId, post_action: this.postAction, post_edit: this.postEdit, + other_user_ids: this.otherUserIds, }); }).finally(() => this.set("suspending", false)); }, diff --git a/app/assets/javascripts/admin/addon/models/web-hook.js b/app/assets/javascripts/admin/addon/models/web-hook.js index 1b74d50b7b..500311bec8 100644 --- a/app/assets/javascripts/admin/addon/models/web-hook.js +++ b/app/assets/javascripts/admin/addon/models/web-hook.js @@ -15,7 +15,7 @@ export default RestModel.extend({ groupsFilterInName: null, @discourseComputed("wildcard_web_hook") - webHookType: { + webhookType: { get(wildcard) { return wildcard ? "wildcard" : "individual"; }, diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js index cb05c26580..1f7ea93a69 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups-index.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups-index.js @@ -1,14 +1,14 @@ import Backup from "admin/models/backup"; import Route from "@ember/routing/route"; +import { bind } from "discourse-common/utils/decorators"; export default Route.extend({ activate() { - this.messageBus.subscribe("/admin/backups", (backups) => - this.controller.set( - "model", - backups.map((backup) => Backup.create(backup)) - ) - ); + this.messageBus.subscribe("/admin/backups", this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe("/admin/backups", this.onMessage); }, model() { @@ -17,7 +17,11 @@ export default Route.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe("/admin/backups"); + @bind + onMessage(backups) { + this.controller.set( + "model", + backups.map((backup) => Backup.create(backup)) + ); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-backups.js b/app/assets/javascripts/admin/addon/routes/admin-backups.js index c78ef4ca1c..1379a09754 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-backups.js +++ b/app/assets/javascripts/admin/addon/routes/admin-backups.js @@ -10,46 +10,19 @@ import { extractError } from "discourse/lib/ajax-error"; import getURL from "discourse-common/lib/get-url"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; +import { bind } from "discourse-common/utils/decorators"; + const LOG_CHANNEL = "/admin/backups/logs"; export default DiscourseRoute.extend({ dialog: service(), activate() { - this.messageBus.subscribe(LOG_CHANNEL, (log) => { - if (log.message === "[STARTED]") { - User.currentProp("hideReadOnlyAlert", true); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - true - ); - this.controllerFor("adminBackupsLogs").get("logs").clear(); - } else if (log.message === "[FAILED]") { - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - this.dialog.alert( - I18n.t("admin.backups.operations.failed", { - operation: log.operation, - }) - ); - } else if (log.message === "[SUCCESS]") { - User.currentProp("hideReadOnlyAlert", false); - this.controllerFor("adminBackups").set( - "model.isOperationRunning", - false - ); - if (log.operation === "restore") { - // redirect to homepage when the restore is done (session might be lost) - window.location = getURL("/"); - } - } else { - this.controllerFor("adminBackupsLogs") - .get("logs") - .pushObject(EmberObject.create(log)); - } - }); + this.messageBus.subscribe(LOG_CHANNEL, this.onMessage); + }, + + deactivate() { + this.messageBus.unsubscribe(LOG_CHANNEL, this.onMessage); }, model() { @@ -64,8 +37,31 @@ export default DiscourseRoute.extend({ ); }, - deactivate() { - this.messageBus.unsubscribe(LOG_CHANNEL); + @bind + onMessage(log) { + if (log.message === "[STARTED]") { + User.currentProp("hideReadOnlyAlert", true); + this.controllerFor("adminBackups").set("model.isOperationRunning", true); + this.controllerFor("adminBackupsLogs").get("logs").clear(); + } else if (log.message === "[FAILED]") { + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + this.dialog.alert( + I18n.t("admin.backups.operations.failed", { + operation: log.operation, + }) + ); + } else if (log.message === "[SUCCESS]") { + User.currentProp("hideReadOnlyAlert", false); + this.controllerFor("adminBackups").set("model.isOperationRunning", false); + if (log.operation === "restore") { + // redirect to homepage when the restore is done (session might be lost) + window.location = getURL("/"); + } + } else { + this.controllerFor("adminBackupsLogs") + .get("logs") + .pushObject(EmberObject.create(log)); + } }, actions: { diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 9b888f9ce5..37272c1c07 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -123,7 +123,7 @@ export default function () { { path: "/web_hooks", resetNamespace: true }, function () { this.route("show", { path: "/:web_hook_id" }); - this.route("showEvents", { path: "/:web_hook_id/events" }); + this.route("edit", { path: "/:web_hook_id/edit" }); } ); }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js new file mode 100644 index 0000000000..6bd96b117b --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-edit.js @@ -0,0 +1,28 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + serialize(model) { + return { web_hook_id: model.id || "new" }; + }, + + model(params) { + if (params.web_hook_id === "new") { + return this.store.createRecord("web-hook"); + } + + return this.store.find("web-hook", params.web_hook_id); + }, + + setupController(controller, model) { + this._super(...arguments); + + if (model.get("isNew")) { + model.set( + "web_hook_event_types", + this.controllerFor("adminWebHooks").defaultEventTypes + ); + } + + controller.set("saved", false); + }, +}); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js deleted file mode 100644 index 1b7923224b..0000000000 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show-events.js +++ /dev/null @@ -1,21 +0,0 @@ -import DiscourseRoute from "discourse/routes/discourse"; -import { get } from "@ember/object"; - -export default DiscourseRoute.extend({ - model(params) { - return this.store.findAll("web-hook-event", get(params, "web_hook_id")); - }, - - setupController(controller, model) { - controller.set("model", model); - controller.subscribe(); - }, - - deactivate() { - this.controllerFor("adminWebHooks.showEvents").unsubscribe(); - }, - - renderTemplate() { - this.render("admin/templates/web-hooks-show-events", { into: "adminApi" }); - }, -}); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js index d76be7f5df..369bbd0059 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks-show.js @@ -1,26 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; -import { get } from "@ember/object"; export default DiscourseRoute.extend({ - serialize(model) { - return { web_hook_id: model.get("id") || "new" }; - }, - model(params) { - if (params.web_hook_id === "new") { - return this.store.createRecord("web-hook"); - } - return this.store.find("web-hook", get(params, "web_hook_id")); - }, - - setupController(controller, model) { - if (model.get("isNew")) { - model.set("web_hook_event_types", controller.get("defaultEventTypes")); - } - - model.set("category_ids", model.get("category_ids")); - model.set("tag_names", model.get("tag_names")); - model.set("group_ids", model.get("group_ids")); - controller.setProperties({ model, saved: false }); + return this.store.find("web-hook", params.web_hook_id); }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js b/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js index 4b20731136..3619142a4c 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js +++ b/app/assets/javascripts/admin/addon/routes/admin-web-hooks.js @@ -1,4 +1,5 @@ import Route from "@ember/routing/route"; + export default Route.extend({ model() { return this.store.findAll("web-hook"); diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs new file mode 100644 index 0000000000..ccb35da4d8 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/components/admin-penalty-similar-users.hbs @@ -0,0 +1,35 @@ ++ {{i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username)}} +
+ +| + | {{i18n "username"}} | +{{i18n "last_seen"}} | +{{i18n "admin.user.topics_entered"}} | +{{i18n "admin.user.posts_read_count"}} | +{{i18n "admin.user.time_read"}} | +{{i18n "created"}} | +
|---|---|---|---|---|---|---|
| + + | +{{avatar user imageSize="small"}} {{user.username}} | +{{format-duration user.last_seen_age}} | +{{number user.topics_entered}} | +{{number user.posts_read_count}} | +{{format-duration user.time_read}} | +{{format-duration user.created_at_age}} | +
{{this.details}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs deleted file mode 100644 index 7e61c6b327..0000000000 --- a/app/assets/javascripts/admin/addon/templates/components/admin-web-hook-event.hbs +++ /dev/null @@ -1,19 +0,0 @@ -{{this.headers}}
- {{this.body}}
- {{i18n "admin.web_hooks.detailed_instruction"}}
+ + + +{{i18n "admin.web_hooks.instruction"}}
+ +| {{i18n "admin.web_hooks.delivery_status.title"}} | +{{i18n "admin.web_hooks.payload_url"}} | +{{i18n "admin.web_hooks.description_label"}} | +{{i18n "admin.web_hooks.controls"}} | +
|---|---|---|---|
|
+ |
+
+ |
+ {{webhook.description}} | +
+ |
+
{{i18n "admin.web_hooks.none"}}
+ {{/if}} +{{i18n "admin.web_hooks.detailed_instruction"}}
-${description}
` })); + } - if (attrs.cooked) { - contents.push( - new RawHtml({ - html: ``, - }) - ); - } + if (attrs.canRecover) { + buttons.push( + this.attach("button", { + className: "btn-flat small-action-recover", + icon: "undo", + action: "recoverPost", + title: "post.controls.undelete", + }) + ); + } + + if (attrs.canEdit && !attrs.canRecover) { + buttons.push( + this.attach("button", { + className: "btn-flat small-action-edit", + icon: "pencil-alt", + action: "editPost", + title: "post.controls.edit", + }) + ); + } + + if (attrs.canDelete) { + buttons.push( + this.attach("button", { + className: "btn-flat btn-danger small-action-delete", + icon: "trash-alt", + action: "deletePost", + title: "post.controls.delete", + }) + ); + } + + if (!attrs.actionDescriptionWidget && attrs.cooked) { + customMessage.push( + new RawHtml({ + html: ``, + }) + ); } return [ h("div.topic-avatar", iconNode(icons[attrs.actionCode] || "exclamation")), - h("div.small-action-desc", contents), + h("div.small-action-desc", [ + h("div.small-action-contents", contents), + h("div.small-action-buttons", buttons), + customMessage, + ]), ]; }, }); diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js index ab636a93ff..bc88e254e5 100644 --- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js +++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js @@ -364,7 +364,7 @@ createWidget("search-menu-results", { if (["topic"].includes(rt.type)) { const more = buildMoreNode(rt); if (more) { - resultNodeContents.push(h("div.show-more", more)); + resultNodeContents.push(h("div.search-menu__show-more", more)); } } diff --git a/app/assets/javascripts/discourse/app/widgets/topic-status.js b/app/assets/javascripts/discourse/app/widgets/topic-status.js index 11d3e35030..39c6de6340 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-status.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-status.js @@ -15,7 +15,7 @@ export default createWidget("topic-status", { const result = []; TopicStatusIcons.render(topic, function (name, key) { - const iconArgs = key === "unpinned" ? { class: "unpinned" } : null; + const iconArgs = { class: key === "unpinned" ? "unpinned" : null }; const icon = iconNode(name, iconArgs); const attributes = { diff --git a/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js b/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js index ec814670ad..01dd33b7bf 100644 --- a/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js +++ b/app/assets/javascripts/discourse/app/widgets/user-status-bubble.js @@ -8,7 +8,7 @@ export default createWidget("user-status-bubble", { let title = attrs.description; if (attrs.ends_at) { const until = moment - .tz(attrs.ends_at, this.currentUser.timezone) + .tz(attrs.ends_at, this.currentUser.user_option.timezone) .format(I18n.t("dates.long_date_without_year")); title += `\n${I18n.t("until")} ${until}`; } diff --git a/app/assets/javascripts/discourse/config/deprecation-workflow.js b/app/assets/javascripts/discourse/config/deprecation-workflow.js index be92c06b23..f0763859a5 100644 --- a/app/assets/javascripts/discourse/config/deprecation-workflow.js +++ b/app/assets/javascripts/discourse/config/deprecation-workflow.js @@ -23,29 +23,5 @@ globalThis.deprecationWorkflow.config = { handler: "silence", matchId: "ember.built-in-components.legacy-arguments", }, - { - handler: "throw", - matchId: "ember-modifier.use-modify", - }, - { - handler: "throw", - matchId: "ember-modifier.use-destroyables", - }, - { - handler: "throw", - matchId: "ember-modifier.no-args-property", - }, - { - handler: "throw", - matchId: "ember-modifier.no-element-property", - }, - { - handler: "throw", - matchId: "ember-modifier.function-based-options", - }, - { - handler: "throw", - matchId: "ember-modifier.function-based-options", - }, ], }; diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index ef7546e0f0..92e993498a 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -29,12 +29,12 @@ "@glimmer/syntax": "^0.84.2", "@glimmer/tracking": "^1.1.2", "@popperjs/core": "^2.11.6", - "@uppy/aws-s3": "^2.2.1", - "@uppy/aws-s3-multipart": "^2.4.1", - "@uppy/core": "^2.3.1", - "@uppy/drop-target": "^1.1.3", - "@uppy/utils": "^4.1.0", - "@uppy/xhr-upload": "^2.1.2", + "@uppy/aws-s3": "^3.0.4", + "@uppy/aws-s3-multipart": "^3.1.1", + "@uppy/core": "^3.0.4", + "@uppy/drop-target": "^2.0.1", + "@uppy/utils": "^5.1.1", + "@uppy/xhr-upload": "^3.0.4", "a11y-dialog": "7.5.2", "admin": "1.0.0", "babel-import-util": "^1.3.0", @@ -66,13 +66,13 @@ "ember-exam": "^8.0.0", "ember-export-application-global": "^2.0.1", "ember-load-initializers": "^2.1.1", - "ember-modifier": "^3.2.7", + "ember-modifier": "^4.0.0", "ember-on-resize-modifier": "^1.1.0", "ember-qunit": "^6.0.0", "ember-rfc176-data": "^0.3.17", "ember-source": "~3.28.11", "ember-test-selectors": "^6.0.0", - "eslint": "^8.28.0", + "eslint": "^8.29.0", "eslint-plugin-qunit": "^7.3.4", "handlebars": "^4.7.7", "html-entities": "^2.3.3", @@ -87,11 +87,11 @@ "pretty-text": "1.0.0", "qunit": "^2.19.3", "qunit-dom": "^2.0.0", - "sass": "^1.56.1", + "sass": "^1.56.2", "select-kit": "1.0.0", "sinon": "^15.0.0", "source-map": "^0.7.4", - "terser": "^5.16.0", + "terser": "^5.16.1", "tippy.js": "^6.3.7", "util": "^0.12.5", "virtual-dom": "^2.1.1", diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js index 4d0f06f7e9..ae6984c4a7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js @@ -13,7 +13,7 @@ acceptance("Admin - Silence User", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js index 73918cd5e2..5e3c36be94 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js @@ -112,7 +112,7 @@ acceptance("Admin - Suspend User - timeframe choosing", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js new file mode 100644 index 0000000000..ce8a1057a0 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-webhooks-test.js @@ -0,0 +1,72 @@ +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import pretender, { + parsePostData, + response, +} from "discourse/tests/helpers/create-pretender"; + +acceptance("Admin - Webhooks", function (needs) { + needs.user(); + + test("adding a webhook", async function (assert) { + pretender.get("/admin/api/web_hooks", () => { + return response({ + web_hooks: [], + total_rows_web_hooks: 0, + load_more_web_hooks: "/admin/api/web_hooks.json?limit=50&offset=50", + extras: { + content_types: [ + { id: 1, name: "application/json" }, + { id: 2, name: "application/x-www-form-urlencoded" }, + ], + default_event_types: [{ id: 2, name: "post" }], + delivery_statuses: [ + { id: 1, name: "inactive" }, + { id: 2, name: "failed" }, + { id: 3, name: "successful" }, + ], + event_types: [ + { id: 1, name: "topic" }, + { id: 2, name: "post" }, + { id: 3, name: "user" }, + { id: 4, name: "group" }, + ], + }, + }); + }); + + pretender.get("/admin/api/web_hook_events/1", () => { + return response({ + web_hook_events: [], + load_more_web_hook_events: + "/admin/api/web_hook_events/1.json?limit=50&offset=50", + total_rows_web_hook_events: 15, + extras: { web_hook_id: 1 }, + }); + }); + + pretender.post("/admin/api/web_hooks", (request) => { + const data = parsePostData(request.requestBody); + assert.strictEqual( + data.web_hook.payload_url, + "https://example.com/webhook" + ); + + return response({ + web_hook: { + id: 1, + // other attrs + }, + }); + }); + + await visit("/admin/api/web_hooks"); + await click(".admin-webhooks__new-button"); + + await fillIn(`[name="payload-url"`, "https://example.com/webhook"); + await click(".admin-webhooks__save-button"); + + assert.strictEqual(currentURL(), "/admin/api/web_hooks/1"); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js index e0c08690dd..8a65eda13a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js @@ -170,7 +170,10 @@ acceptance("Bookmarking", function (needs) { await selectKit(".bookmark-option-selector").selectRowByValue(1); await click("#save-bookmark"); - assert.equal(User.current().bookmark_auto_delete_preference, "1"); + assert.equal( + User.currentProp("user_option.bookmark_auto_delete_preference"), + "1" + ); await openEditBookmarkModal(); @@ -243,7 +246,7 @@ acceptance("Bookmarking", function (needs) { test("Editing a bookmark", async function (assert) { await visit("/t/internationalization-localization/280"); - let now = moment.tz(loggedInUser().timezone); + let now = moment.tz(loggedInUser().user_option.timezone); let tomorrow = now.add(1, "day").format("YYYY-MM-DD"); await openBookmarkModal(); await fillIn("input#bookmark-name", "Test name"); @@ -269,7 +272,7 @@ acceptance("Bookmarking", function (needs) { test("Using a post date for the reminder date", async function (assert) { await visit("/t/internationalization-localization/280"); - let postDate = moment.tz("2036-01-15", loggedInUser().timezone); + let postDate = moment.tz("2036-01-15", loggedInUser().user_option.timezone); let postDateFormatted = postDate.format("YYYY-MM-DD"); await openBookmarkModal(); await fillIn("input#bookmark-name", "Test name"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js index 29528b624a..41f650c48a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js @@ -126,7 +126,7 @@ acceptance("Composer - editor mentions", function (needs) { }); test("shows status on search results when mentioning a user", async function (assert) { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; const now = moment(status.ends_at).add(-1, "hour").format(); clock = fakeTime(now, timezone, true); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js index 75103a3c9b..c70bd81d92 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js @@ -20,12 +20,12 @@ acceptance("Composer - Image Preview", function (needs) { server.get("/posts/419", () => { return helper.response({ id: 419 }); }); - server.get("/u/is_local_username", () => { + server.get("/composer/mentions", () => { return helper.response({ - valid: [], - valid_groups: ["staff"], - mentionable_groups: [{ name: "staff", user_count: 30 }], - cannot_see: [], + users: [], + user_reasons: {}, + groups: { staff: { user_count: 30 } }, + group_reasons: {}, max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js index 25f9cae229..8d78ac5114 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js @@ -3,7 +3,7 @@ import { exists, query, } from "discourse/tests/helpers/qunit-helpers"; -import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; import I18n from "I18n"; @@ -39,3 +39,60 @@ acceptance("Composer - Messages", function (needs) { ); }); }); + +acceptance("Composer - Messages - Cannot see group", function (needs) { + needs.user(); + needs.pretender((server, helper) => { + server.get("/composer/mentions", () => { + return helper.response({ + users: [], + user_reasons: {}, + groups: { + staff: { user_count: 30 }, + staff2: { user_count: 30, notified_count: 10 }, + }, + group_reasons: { staff: "not_allowed", staff2: "some_not_allowed" }, + max_users_notified_per_group_mention: 100, + }); + }); + }); + + test("Shows warning in composer if group hasn't been invited", async function (assert) { + await visit("/t/130"); + await click("button.create"); + assert.ok( + !exists(".composer-popup"), + "composer warning is not shown by default" + ); + await fillIn(".d-editor-input", "Mention @staff"); + assert.ok(exists(".composer-popup"), "shows composer warning message"); + assert.ok( + query(".composer-popup").innerHTML.includes( + I18n.t("composer.cannot_see_group_mention.not_allowed", { + group: "staff", + }) + ), + "warning message has correct body" + ); + }); + + test("Shows warning in composer if group hasn't been invited, but some members have access already", async function (assert) { + await visit("/t/130"); + await click("button.create"); + assert.ok( + !exists(".composer-popup"), + "composer warning is not shown by default" + ); + await fillIn(".d-editor-input", "Mention @staff2"); + assert.ok(exists(".composer-popup"), "shows composer warning message"); + assert.ok( + query(".composer-popup").innerHTML.includes( + I18n.t("composer.cannot_see_group_mention.some_not_allowed", { + group: "staff2", + count: 10, + }) + ), + "warning message has correct body" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index b250106bd9..9637fd77d2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -70,12 +70,12 @@ acceptance("Composer", function (needs) { server.get("/posts/419", () => { return helper.response({ id: 419 }); }); - server.get("/u/is_local_username", () => { + server.get("/composer/mentions", () => { return helper.response({ - valid: [], - valid_groups: ["staff"], - mentionable_groups: [{ name: "staff", user_count: 30 }], - cannot_see: [], + users: [], + user_reasons: {}, + groups: { staff: { user_count: 30 } }, + group_reasons: {}, max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js index 0cc847176e..ce818cb6fc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js @@ -191,7 +191,7 @@ acceptance( }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js new file mode 100644 index 0000000000..a6f7035d51 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/post-inline-mentions-test.js @@ -0,0 +1,160 @@ +import { + acceptance, + exists, + publishToMessageBus, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import { visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "../fixtures/topic"; +import pretender, { response } from "discourse/tests/helpers/create-pretender"; + +acceptance("Post inline mentions test", function (needs) { + needs.user(); + + const topicId = 130; + const mentionedUserId = 1; + const status = { + description: "Surfing", + emoji: "surfing_man", + ends_at: null, + }; + + function topicWithoutUserStatus() { + const topic = cloneJSON(topicFixtures[`/t/${topicId}.json`]); + const firstPost = topic.post_stream.posts[0]; + firstPost.cooked = + 'I am mentioning @user1 again.
'; + firstPost.mentioned_users = [ + { + id: mentionedUserId, + username: "user1", + avatar_template: "/letter_avatar_proxy/v4/letter/a/bbce88/{size}.png", + }, + ]; + return topic; + } + + function topicWithUserStatus() { + const topic = topicWithoutUserStatus(); + topic.post_stream.posts[0].mentioned_users[0].status = status; + return topic; + } + + test("shows user status on inline mentions", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + status.description, + "status description is correct" + ); + assert.ok( + statusElement.src.includes(status.emoji), + "status emoji is correct" + ); + }); + + test("inserts user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithoutUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.notOk( + exists(".topic-post .cooked .mention .user-status"), + "user status isn't shown" + ); + + await publishToMessageBus("/user-status", { + [mentionedUserId]: { + description: status.description, + emoji: status.emoji, + }, + }); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + status.description, + "status description is correct" + ); + assert.ok( + statusElement.src.includes(status.emoji), + "status emoji is correct" + ); + }); + + test("updates user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "initial user status is shown" + ); + + const newStatus = { + description: "off to dentist", + emoji: "tooth", + }; + await publishToMessageBus("/user-status", { + [mentionedUserId]: { + description: newStatus.description, + emoji: newStatus.emoji, + }, + }); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "updated user status is shown" + ); + const statusElement = query(".topic-post .cooked .mention .user-status"); + assert.equal( + statusElement.title, + newStatus.description, + "updated status description is correct" + ); + assert.ok( + statusElement.src.includes(newStatus.emoji), + "updated status emoji is correct" + ); + }); + + test("removes user status on message bus message", async function (assert) { + pretender.get(`/t/${topicId}.json`, () => { + return response(topicWithUserStatus()); + }); + await visit(`/t/lorem-ipsum-dolor-sit-amet/${topicId}`); + + assert.ok( + exists(".topic-post .cooked .mention .user-status"), + "initial user status is shown" + ); + + await publishToMessageBus("/user-status", { + [mentionedUserId]: null, + }); + + assert.notOk( + exists(".topic-post .cooked .mention .user-status"), + "updated user has disappeared" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js index 6f60d307d7..9b6e3089cb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js @@ -111,11 +111,6 @@ acceptance("User Preferences", function (needs) { "apps tab isn't there when you have no authorized apps" ); }); - - test("username", async function (assert) { - await visit("/u/eviltrout/preferences/username"); - assert.ok(exists("#change_username"), "it has the input element"); - }); }); acceptance("Custom User Fields", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js b/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js index ac40d43db0..15e453e82e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/redirect-to-top-test.js @@ -19,10 +19,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects categories to weekly top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: "weekly", - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: "weekly", + reason: "Welcome back!", + }, }, }); @@ -36,10 +38,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects latest to monthly top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: "monthly", - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: "monthly", + reason: "Welcome back!", + }, }, }); @@ -53,10 +57,12 @@ acceptance("Redirect to Top", function (needs) { test("redirects root to All top", async function (assert) { updateCurrentUser({ - should_be_redirected_to_top: true, - redirected_to_top: { - period: null, - reason: "Welcome back!", + user_option: { + should_be_redirected_to_top: true, + redirected_to_top: { + period: null, + reason: "Welcome back!", + }, }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js index 0d8c4cc0c6..a1a04511eb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js @@ -44,12 +44,6 @@ const RESPONSES = { security_keys_enabled: true, allowed_methods: [BACKUP_CODE], }, - ok010010: { - totp_enabled: false, - backup_enabled: true, - security_keys_enabled: false, - allowed_methods: [BACKUP_CODE], - }, }; Object.keys(RESPONSES).forEach((k) => { @@ -184,14 +178,6 @@ acceptance("Second Factor Auth Page", function (needs) { !exists(".toggle-second-factor-method"), "no alternative methods are shown if only 1 method is allowed" ); - - // only backup codes - await visit("/session/2fa?nonce=ok010010"); - assert.ok(exists("form.backup-code-token"), "backup code form is shown"); - assert.ok( - !exists(".toggle-second-factor-method"), - "no alternative methods are shown if only 1 method is allowed" - ); }); test("switching 2FA methods", async function (assert) { @@ -299,8 +285,7 @@ acceptance("Second Factor Auth Page", function (needs) { }); test("sidebar is disabled on 2FA route", async function (assert) { - this.siteSettings.enable_experimental_sidebar_hamburger = true; - this.siteSettings.enable_sidebar = true; + this.siteSettings.navigation_menu = "sidebar"; await visit("/session/2fa?nonce=ok110111"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js index 9ec6b59826..252a8e756b 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-categories-section-test.js @@ -9,8 +9,7 @@ import Site from "discourse/models/site"; acceptance("Sidebar - Anonymous - Categories Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("category section links ordered by category's topic count when default_sidebar_categories has not been configured and site setting to fix categories positions is disabled", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js index c01d2357d9..28d186a8f5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-community-section-test.js @@ -12,8 +12,7 @@ import { click, visit } from "@ember/test-helpers"; acceptance("Sidebar - Anonymous user - Community Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("display short site description site setting when it is set", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js index 09d805069b..715f08aa5f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-tags-section-test.js @@ -9,8 +9,7 @@ import Site from "discourse/models/site"; acceptance("Sidebar - Anonymous Tags Section", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", suppress_uncategorized_badge: false, tagging_enabled: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js index d350dd1aaa..f6de25ef9d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-anonymous-user-test.js @@ -6,8 +6,7 @@ import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; acceptance("Sidebar - Anonymous User", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("sidebar is displayed", async function (assert) { @@ -30,7 +29,7 @@ acceptance("Sidebar - Anonymous User", function (needs) { }); test("sidebar hamburger panel dropdown when sidebar has been disabled", async function (assert) { - this.siteSettings.enable_sidebar = false; + this.siteSettings.navigation_menu = "header dropdown"; await visit("/"); await click(".hamburger-dropdown"); @@ -44,8 +43,7 @@ acceptance("Sidebar - Anonymous User", function (needs) { acceptance("Sidebar - Anonymous User - Login Required", function (needs) { needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", login_required: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js index 3241278e81..071a1bb5cc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-mobile-test.js @@ -9,8 +9,7 @@ acceptance("Sidebar - Mobile - User with sidebar enabled", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.mobileView(); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js index 60d1f38184..7ac9ef275f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-narrow-desktop-test.js @@ -8,8 +8,7 @@ acceptance("Sidebar - Narrow Desktop", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("wide sidebar is changed to cloak when resize to narrow screen", async function (assert) { @@ -55,4 +54,30 @@ acceptance("Sidebar - Narrow Desktop", function (needs) { bodyElement.style.width = null; }); + + test("transition from narrow screen to wide screen", async function (assert) { + await visit("/"); + await settled(); + + const bodyElement = document.querySelector("body"); + bodyElement.style.width = "990px"; + + await waitUntil( + () => document.querySelector(".btn-sidebar-toggle.narrow-desktop"), + { + timeout: 5000, + } + ); + await click(".btn-sidebar-toggle"); + + bodyElement.style.width = "1200px"; + await waitUntil(() => document.querySelector("#d-sidebar"), { + timeout: 5000, + }); + await click(".header-dropdown-toggle.current-user"); + $(".header-dropdown-toggle.current-user").click(); + assert.ok(exists(".quick-access-panel")); + + bodyElement.style.width = null; + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js index 511f6f1b29..489972f675 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js @@ -14,8 +14,7 @@ acceptance("Sidebar - Plugin API", function (needs) { needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.hooks.afterEach(() => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js index 3b4a14cd4c..dd182b99b9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-categories-section-test.js @@ -22,8 +22,7 @@ acceptance( function (needs) { needs.settings({ allow_uncategorized_topics: false, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user({ admin: false }); @@ -66,8 +65,7 @@ acceptance("Sidebar - Logged on user - Categories Section", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", suppress_uncategorized_badge: false, allow_uncategorized_topics: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js index 47c505d2f3..41756649fe 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-community-section-test.js @@ -28,8 +28,7 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.pretender((server, helper) => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js index fb7c120e10..6ae2256161 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-messages-section-test.js @@ -22,8 +22,7 @@ acceptance( }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("clicking on section header button", async function (assert) { @@ -43,8 +42,7 @@ acceptance( needs.user({ can_send_private_messages: true }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.pretender((server, helper) => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js index 7b80114b9c..018d7d3b23 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-tags-section-test.js @@ -19,8 +19,7 @@ acceptance( function (needs) { needs.settings({ tagging_enabled: false, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user(); @@ -39,8 +38,7 @@ acceptance( acceptance("Sidebar - Logged on user - Tags section", function (needs) { needs.settings({ tagging_enabled: true, - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); needs.user({ diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js index 8f80d85dc3..ed2b4c5db3 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-user-test.js @@ -14,7 +14,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: false, + navigation_menu: "legacy", }); test("clicking header hamburger icon displays old hamburger dropdown", async function (assert) { @@ -32,8 +32,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: false, + navigation_menu: "header dropdown", }); test("showing and hiding sidebar", async function (assert) { @@ -80,8 +79,7 @@ acceptance( needs.user(); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", }); test("viewing keyboard shortcuts using sidebar", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js index 9ff9d542db..0193efa000 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js @@ -23,7 +23,10 @@ acceptance("Topic - Bulk Actions", function (needs) { }); test("bulk select - modal", async function (assert) { - updateCurrentUser({ moderator: true, enable_defer: true }); + updateCurrentUser({ + moderator: true, + user_option: { enable_defer: true }, + }); await visit("/latest"); await click("button.bulk-select"); @@ -168,7 +171,13 @@ acceptance("Topic - Bulk Actions", function (needs) { }); test("TL4 users can bulk select", async function (assert) { - updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + updateCurrentUser({ + moderator: false, + admin: false, + trust_level: 4, + user_option: { enable_defer: false }, + }); + await visit("/latest"); await click("button.bulk-select"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js index 39d77bdd8b..728de641e5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js @@ -11,6 +11,8 @@ import { click, fillIn, visit } from "@ember/test-helpers"; import { test } from "qunit"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import I18n from "I18n"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "discourse/tests/fixtures/topic"; acceptance("Topic - Edit timer", function (needs) { let clock = null; @@ -28,10 +30,14 @@ acceptance("Topic - Edit timer", function (needs) { category_id: null, }) ); + + const topicResponse = cloneJSON(topicFixtures["/t/54077.json"]); + topicResponse.details.can_delete = false; + server.get("/t/54077.json", () => helper.response(topicResponse)); }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; const tuesday = "2100-06-15T08:00:00"; clock = fakeTime(tuesday, timezone, true); }); @@ -294,7 +300,7 @@ acceptance("Topic - Edit timer", function (needs) { test("TL4 can't auto-delete", async function (assert) { updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); - await visit("/t/internationalization-localization"); + await visit("/t/short-topic-with-two-posts/54077"); await click(".toggle-admin-menu"); await click(".admin-topic-timer-update button"); @@ -305,6 +311,48 @@ acceptance("Topic - Edit timer", function (needs) { assert.ok(!timerType.rowByValue("delete").exists()); }); + test("Category Moderator can auto-delete replies", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/internationalization-localization"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(timerType.rowByValue("delete_replies").exists()); + }); + + test("TL4 can't auto-delete replies", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/short-topic-with-two-posts/54077"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(!timerType.rowByValue("delete_replies").exists()); + }); + + test("Category Moderator can auto-delete", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 4 }); + + await visit("/t/internationalization-localization"); + await click(".toggle-admin-menu"); + await click(".admin-topic-timer-update button"); + + const timerType = selectKit(".select-kit.timer-type"); + + await timerType.expand(); + + assert.ok(timerType.rowByValue("delete").exists()); + }); + test("auto delete", async function (assert) { updateCurrentUser({ moderator: true }); const timerType = selectKit(".select-kit.timer-type"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js index e35eb72071..890dc80b74 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js @@ -29,7 +29,7 @@ acceptance("Topic - Set Slow Mode", function (needs) { }); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js index bfd7611d4a..43904e65b7 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js @@ -12,11 +12,6 @@ import { cloneJSON } from "discourse-common/lib/object"; acceptance("User Card - Show Local Time", function (needs) { needs.user(); needs.settings({ display_local_time_in_user_card: true }); - needs.pretender((server, helper) => { - const cardResponse = cloneJSON(userFixtures["/u/charlie/card.json"]); - delete cardResponse.user.timezone; - server.get("/u/charlie/card.json", () => helper.response(cardResponse)); - }); test("user card local time - does not update timezone for another user", async function (assert) { User.current().timezone = "Australia/Brisbane"; diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js index e33b9ccb45..83d8162c6c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-account-test.js @@ -1,14 +1,17 @@ import { test } from "qunit"; import I18n from "I18n"; import sinon from "sinon"; -import { click, visit } from "@ember/test-helpers"; +import { click, fillIn, visit } from "@ember/test-helpers"; import { acceptance, exists, query, } from "discourse/tests/helpers/qunit-helpers"; import DiscourseURL from "discourse/lib/url"; -import { fixturesByUrl } from "discourse/tests/helpers/create-pretender"; +import pretender, { + fixturesByUrl, + response, +} from "discourse/tests/helpers/create-pretender"; import { cloneJSON } from "discourse-common/lib/object"; acceptance("User Preferences - Account", function (needs) { @@ -58,6 +61,41 @@ acceptance("User Preferences - Account", function (needs) { pickAvatarRequestData = null; }); + test("changing username", async function (assert) { + const stub = sinon + .stub(DiscourseURL, "redirectTo") + .withArgs("/u/good_trout/preferences"); + + pretender.put("/u/eviltrout/preferences/username", (data) => { + assert.strictEqual(data.requestBody, "new_username=good_trout"); + + return response({ + id: fixturesByUrl["/u/eviltrout.json"].user.id, + username: "good_trout", + }); + }); + + await visit("/u/eviltrout/preferences/account"); + + assert.strictEqual( + query(".username-preference__current-username").innerText, + "eviltrout" + ); + + await click(".username-preference__edit-username"); + + assert.strictEqual(query(".username-preference__input").value, "eviltrout"); + assert.true(query(".username-preference__submit").disabled); + + await fillIn(query(".username-preference__input"), "good_trout"); + assert.false(query(".username-preference__submit").disabled); + + await click(".username-preference__submit"); + await click(".dialog-container .btn-primary"); + + sinon.assert.calledOnce(stub); + }); + test("Delete dialog", async function (assert) { sinon.stub(DiscourseURL, "redirectAbsolute"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js index d107673ac2..b4f1996ba9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js @@ -116,7 +116,7 @@ acceptance("User Notifications - Users - Ignore User", function (needs) { needs.user(); needs.hooks.beforeEach(() => { - const timezone = loggedInUser().timezone; + const timezone = loggedInUser().user_option.timezone; clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js index 1bf8cfa1f4..344ff4b607 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-profile-test.js @@ -5,6 +5,8 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import { click, visit } from "@ember/test-helpers"; import { test } from "qunit"; +import { cloneJSON } from "discourse-common/lib/object"; +import userFixtures from "discourse/tests/fixtures/user-fixtures"; acceptance("User - Preferences - Profile - Featured topic", function (needs) { needs.user(); @@ -65,7 +67,15 @@ acceptance("User - Preferences - Profile - Featured topic", function (needs) { acceptance( "User - Preferences - Profile - No default calendar set", function (needs) { - needs.user({ default_calendar: "none_selected" }); + needs.user(); + + needs.pretender((server, helper) => { + server.get("/u/eviltrout.json", () => { + const cloned = cloneJSON(userFixtures["/u/eviltrout.json"]); + cloned.user.user_option.default_calendar = "none_selected"; + return helper.response(200, cloned); + }); + }); test("default calendar option is not visible", async function (assert) { await visit("/u/eviltrout/preferences/profile"); @@ -81,7 +91,15 @@ acceptance( acceptance( "User - Preferences - Profile - Default calendar set", function (needs) { - needs.user({ default_calendar: "google" }); + needs.user(); + + needs.pretender((server, helper) => { + server.get("/u/eviltrout.json", () => { + const cloned = cloneJSON(userFixtures["/u/eviltrout.json"]); + cloned.user.user_option.default_calendar = "google"; + return helper.response(200, cloned); + }); + }); test("default calendar can be changed", async function (assert) { await visit("/u/eviltrout/preferences/profile"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js index 218823c676..ebd73c0bc4 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-backup-test.js @@ -3,6 +3,7 @@ import { click, visit } from "@ember/test-helpers"; import { acceptance, exists, + query, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -42,4 +43,17 @@ acceptance("User Preferences - Second Factor Backup", function (needs) { assert.ok(exists(".backup-codes-area"), "shows backup codes"); }); + + test("delete backup codes", async function (assert) { + updateCurrentUser({ second_factor_enabled: true }); + await visit("/u/eviltrout/preferences/second-factor"); + await click(".edit-2fa-backup"); + await click(".second-factor-backup-preferences .btn-primary"); + await click(".modal-close"); + await click(".pref-second-factor-backup .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting backup codes" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js index d5b9a2630e..6f8a1dc810 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js @@ -4,6 +4,7 @@ import { acceptance, exists, query, + updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; acceptance("User Preferences - Second Factor", function (needs) { @@ -14,6 +15,8 @@ acceptance("User Preferences - Second Factor", function (needs) { return helper.response({ success: "OK", password_required: "true", + totps: [{ id: 1, name: "one of them" }], + security_keys: [{ id: 2, name: "key" }], }); }); @@ -90,4 +93,33 @@ acceptance("User Preferences - Second Factor", function (needs) { ); } }); + + test("delete second factor security method", async function (assert) { + updateCurrentUser({ moderator: false, admin: false, trust_level: 1 }); + await visit("/u/eviltrout/preferences/second-factor"); + + assert.ok(exists("#password"), "it has a password input"); + + await fillIn("#password", "secrets"); + await click(".user-preferences .btn-primary"); + await click(".totp .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting an authenticator" + ); + await click(".dialog-close"); + + await click(".security-key .btn-danger"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Deleting an authenticator" + ); + await click(".dialog-close"); + + await click(".btn-danger.btn-icon-text"); + assert.strictEqual( + query("#dialog-title").innerText.trim(), + "Are you sure you want to disable two-factor authentication?" + ); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js index 85db3fbb62..02eb2773dd 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js @@ -17,8 +17,7 @@ acceptance("User Preferences - Sidebar", function (needs) { }); needs.settings({ - enable_experimental_sidebar_hamburger: true, - enable_sidebar: true, + navigation_menu: "sidebar", tagging_enabled: true, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js index 92016c8e37..81be6acbbf 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-tracking-test.js @@ -189,6 +189,8 @@ acceptance("User Preferences - Tracking", function (needs) { await click(".save-changes"); assert.deepEqual(putRequestData, { + auto_track_topics_after_msecs: "60000", + new_topic_duration_minutes: "1440", "regular_category_ids[]": ["-1"], "tracked_category_ids[]": ["4"], "watched_category_ids[]": ["3"], @@ -211,6 +213,8 @@ acceptance("User Preferences - Tracking", function (needs) { await click(".save-changes"); assert.deepEqual(putRequestData, { + auto_track_topics_after_msecs: "60000", + new_topic_duration_minutes: "1440", "muted_category_ids[]": ["-1"], "tracked_category_ids[]": ["4"], "watched_category_ids[]": ["3"], diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js index 8dc4313df4..9ac356da93 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js @@ -17,6 +17,8 @@ import { resetHighestReadCache, setHighestReadCache, } from "discourse/lib/topic-list-tracker"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { resetCustomUserNavMessagesDropdownRows } from "discourse/controllers/user-private-messages"; acceptance( "User Private Messages - user with no group messages", @@ -753,6 +755,30 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { "All tags is still selected in dropdown" ); }); + + test("addUserMessagesNavigationDropdownRow plugin api", async function (assert) { + try { + withPluginApi("1.5.0", (api) => { + api.addUserMessagesNavigationDropdownRow( + "preferences", + "test nav", + "arrow-left" + ); + }); + + await visit("/u/eviltrout/messages"); + + const messagesDropdown = selectKit(".user-nav-messages-dropdown"); + await messagesDropdown.expand(); + + const row = messagesDropdown.rowByName("test nav"); + + assert.strictEqual(row.value(), "/u/eviltrout/preferences"); + assert.ok(row.icon().classList.contains("d-icon-arrow-left")); + } finally { + resetCustomUserNavMessagesDropdownRows(); + } + }); } } diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js index ccef190ca1..9e8e0d14c5 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-status-test.js @@ -26,7 +26,7 @@ acceptance("User Status", function (needs) { const userId = 1; const userTimezone = "UTC"; - needs.user({ id: userId, timezone: userTimezone }); + needs.user({ id: userId, "user_option.timezone": userTimezone }); needs.pretender((server, helper) => { server.put("/user-status.json", () => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js index c84da617f8..e8ce1fad39 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js @@ -1,6 +1,5 @@ import I18n from "I18n"; import EmberObject from "@ember/object"; -import User from "discourse/models/user"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; import userFixtures from "discourse/tests/fixtures/user-fixtures"; @@ -167,21 +166,33 @@ acceptance("User - Saving user options", function (needs) { disable_mailing_list_mode: false, }); + let putRequestData; + needs.pretender((server, helper) => { - server.put("/u/eviltrout.json", () => { - return helper.response(200, { user: {} }); + server.put("/u/eviltrout.json", (request) => { + putRequestData = helper.parsePostData(request.requestBody); + return helper.response({ user: {} }); }); }); - test("saving user options", async function (assert) { - const spy = sinon.spy(User.current(), "_saveUserData"); + needs.hooks.afterEach(() => { + putRequestData = null; + }); + test("saving user options", async function (assert) { await visit("/u/eviltrout/preferences/emails"); await click(".pref-mailing-list-mode input[type='checkbox']"); await click(".save-changes"); - assert.ok( - spy.calledWithMatch({ mailing_list_mode: true }), + assert.deepEqual( + putRequestData, + { + digest_after_minutes: "10080", + email_digests: "true", + email_level: "1", + email_messages_level: "0", + mailing_list_mode: "true", + }, "sends a PUT request to update the specified user option" ); @@ -189,8 +200,15 @@ acceptance("User - Saving user options", function (needs) { await selectKit("#user-email-messages-level").selectRowByValue(2); // never option await click(".save-changes"); - assert.ok( - spy.calledWithMatch({ email_messages_level: 2 }), + assert.deepEqual( + putRequestData, + { + digest_after_minutes: "10080", + email_digests: "true", + email_level: "1", + email_messages_level: "2", + mailing_list_mode: "true", + }, "is able to save a different user_option on a subsequent request" ); }); diff --git a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js index b5430580e8..fe0c2dc870 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/session-fixtures.js @@ -18,21 +18,14 @@ export default { title: "co-founder", reply_count: 859, topic_count: 36, - enable_quoting: true, - external_links_in_new_tab: false, - dynamic_favicon: true, trust_level: 4, can_edit: true, can_invite_to_forum: true, can_send_private_messages: true, - should_be_redirected_to_top: false, custom_fields: {}, muted_category_ids: [], dismissed_banner_key: null, akismet_review_count: 0, - title_count_mode: "notifications", - timezone: "Australia/Brisbane", - skip_new_user_tips: false, can_review: true, ignored_users: [], groups: [ @@ -48,7 +41,16 @@ export default { name: "trust_level_1", display_name: "trust_level_1", } - ] + ], + user_option: { + external_links_in_new_tab: false, + enable_quoting: true, + dynamic_favicon: true, + title_count_mode: "notifications", + timezone: "Australia/Brisbane", + skip_new_user_tips: false, + should_be_redirected_to_top: false, + }, }, }, }; diff --git a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js index 471f7c47fc..979543af5d 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/user-fixtures.js @@ -107,6 +107,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: true, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 19, username: "eviltrout", @@ -166,17 +178,6 @@ export default { can_be_deleted: false, can_delete_all_posts: false, locale: "", - email_digests: true, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [4], tracked_category_ids: [], @@ -294,7 +295,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", has_topic_draft: true, }, }, @@ -2722,8 +2722,8 @@ export default { hide_profile_and_presence: false, text_size: "normal", text_size_seq: 0, + timezone: "America/Los_Angeles", }, - timezone: "America/Los_Angeles", }, }, "/u/charlie/card.json": { @@ -3253,6 +3253,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: true, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 4432, username: "e.il.rout", @@ -3312,17 +3324,6 @@ export default { can_be_deleted: false, can_delete_all_posts: false, locale: "", - email_digests: true, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [], tracked_category_ids: [], @@ -3439,7 +3440,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", }, }, "/u/staged.json": { @@ -3470,6 +3470,18 @@ export default { user: { user_option: { text_size_seq: 1, + email_digests: false, + email_messages_level: 0, + email_level: 1, + digest_after_minutes: 10080, + mailing_list_mode: false, + auto_track_topics_after_msecs: 60000, + new_topic_duration_minutes: 1440, + external_links_in_new_tab: false, + dynamic_favicon: true, + skip_new_user_tips: false, + enable_quoting: true, + timezone: "Australia/Brisbane", }, id: 20, username: "staged", @@ -3509,17 +3521,6 @@ export default { can_be_deleted: true, can_delete_all_posts: true, locale: "", - email_digests: false, - email_messages_level: 0, - email_level: 1, - digest_after_minutes: 10080, - mailing_list_mode: false, - auto_track_topics_after_msecs: 60000, - new_topic_duration_minutes: 1440, - external_links_in_new_tab: false, - dynamic_favicon: true, - skip_new_user_tips: false, - enable_quoting: true, muted_category_ids: [], regular_category_ids: [], tracked_category_ids: [], @@ -3562,7 +3563,6 @@ export default { day_6_start_time: 480, day_6_end_time: 1020, }, - timezone: "Australia/Brisbane", }, }, "/u/recent-searches": { diff --git a/app/assets/javascripts/discourse/tests/helpers/component-test.js b/app/assets/javascripts/discourse/tests/helpers/component-test.js index 4e79dd558a..f4e36d4c98 100644 --- a/app/assets/javascripts/discourse/tests/helpers/component-test.js +++ b/app/assets/javascripts/discourse/tests/helpers/component-test.js @@ -24,7 +24,6 @@ export function setupRenderingTest(hooks) { const currentUser = User.create({ username: "eviltrout", - timezone: "Australia/Brisbane", name: "Robin Ward", admin: false, moderator: false, @@ -42,6 +41,9 @@ export function setupRenderingTest(hooks) { display_name: "trust_level_1", }, ], + user_option: { + timezone: "Australia/Brisbane", + }, }); this.currentUser = currentUser; this.owner.unregister("service:current-user"); diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index da7ab2e5f7..d694826cf5 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -173,12 +173,13 @@ export function applyDefaultHandlers(pretender) { return response({ email: "eviltrout@example.com" }); }); - pretender.get("/u/is_local_username", () => + pretender.get("/composer/mentions", () => response({ - valid: [], - valid_groups: [], - mentionable_groups: [], - cannot_see: [], + users: [], + user_reasons: {}, + groups: {}, + group_reasons: {}, + max_users_notified_per_group_mention: 100, }) ); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 06b4062601..b4d49f2f6d 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -75,6 +75,7 @@ import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections"; import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager"; import { resetUserMenuTabs } from "discourse/lib/user-menu/tab"; import { reset as resetLinkLookup } from "discourse/lib/link-lookup"; +import { resetMentions } from "discourse/lib/link-mentions"; import { resetModelTransformers } from "discourse/lib/model-transformers"; import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper"; @@ -208,6 +209,7 @@ export function testCleanup(container, app) { resetUserMenuTabs(); resetLinkLookup(); resetModelTransformers(); + resetMentions(); cleanupTemporaryModuleRegistrations(); } diff --git a/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js b/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js index eeb58665f2..5d277512ce 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/bookmark-icon-test.js @@ -14,7 +14,7 @@ module("Integration | Component | bookmark-icon", function (hooks) { test("with reminder", async function (assert) { this.setProperties({ bookmark: Bookmark.create({ - reminder_at: tomorrow(this.currentUser.timezone), + reminder_at: tomorrow(this.currentUser.user_option.timezone), name: "some name", }), }); @@ -29,7 +29,7 @@ module("Integration | Component | bookmark-icon", function (hooks) { I18n.t("bookmarks.created_with_reminder_generic", { date: formattedReminderTime( this.bookmark.reminder_at, - this.currentUser.timezone + this.currentUser.user_option.timezone ), name: "some name", }) diff --git a/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js b/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js index 6bd6016b72..28405f9cd3 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/composer-editor-test.js @@ -8,26 +8,28 @@ module("Integration | Component | ComposerEditor", function (hooks) { setupRenderingTest(hooks); test("warns about users that will not see a mention", async function (assert) { - assert.expect(1); + assert.expect(2); this.set("model", {}); this.set("noop", () => {}); - this.set("expectation", (warnings) => { - assert.deepEqual(warnings, [ - { name: "user-no", reason: "a reason" }, - { name: "user-nope", reason: "a reason" }, - ]); + this.set("expectation", (warning) => { + if (warning.name === "user-no") { + assert.deepEqual(warning, { name: "user-no", reason: "a reason" }); + } else if (warning.name === "user-nope") { + assert.deepEqual(warning, { name: "user-nope", reason: "a reason" }); + } }); - pretender.get("/u/is_local_username", () => { + pretender.get("/composer/mentions", () => { return response({ - cannot_see: { + users: ["user-ok", "user-no", "user-nope"], + user_reasons: { "user-no": "a reason", "user-nope": "a reason", }, - mentionable_groups: [], - valid: ["user-ok", "user-no", "user-nope"], - valid_groups: [], + groups: {}, + group_reasons: {}, + max_users_notified_per_group_mention: 100, }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js index 361c55b570..885818fbdb 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/d-document-test.js @@ -23,7 +23,7 @@ module("Integration | Component | d-document", function (hooks) { const titleBefore = document.title; try { this.currentUser.redesigned_user_menu_enabled = true; - this.currentUser.title_count_mode = "notifications"; + this.currentUser.user_option.title_count_mode = "notifications"; await render(hbs`Then visit the following URL to use Discourse:
To disable this warning and allow direct Rails access, start the server with ALLOW_EMBER_CLI_PROXY_BYPASS=1