Merge branch 'main' into 0-reveal-password
This commit is contained in:
commit
02c2f64ae1
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -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'
|
||||
|
||||
38
Gemfile.lock
38
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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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")}`)
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
<label class="hook-event">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.enabled}}
|
||||
name="event-choice"
|
||||
/>
|
||||
|
||||
{{this.name}}
|
||||
|
||||
<p>{{this.details}}</p>
|
||||
</label>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<li>
|
||||
<div class="col first status">
|
||||
<span class={{this.statusColorClasses}}>{{@event.status}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col event-id">{{@event.id}}</div>
|
||||
|
||||
<div class="col timestamp">{{this.createdAt}}</div>
|
||||
|
||||
<div class="col completion">{{this.completion}}</div>
|
||||
|
||||
<div class="col actions">
|
||||
<DButton
|
||||
@icon={{this.expandRequestIcon}}
|
||||
@action={{this.toggleRequest}}
|
||||
@label="admin.web_hooks.events.request"
|
||||
/>
|
||||
<DButton
|
||||
@icon={{this.expandResponseIcon}}
|
||||
@action={{this.toggleResponse}}
|
||||
@label="admin.web_hooks.events.response"
|
||||
/>
|
||||
<DButton
|
||||
@icon="sync"
|
||||
@action={{this.redeliver}}
|
||||
@label="admin.web_hooks.events.redeliver"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.expandDetails}}
|
||||
<div class="details">
|
||||
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
|
||||
<pre><code>{{this.headers}}</code></pre>
|
||||
|
||||
<h3>{{this.bodyLabel}}</h3>
|
||||
<pre><code>{{this.body}}</code></pre>
|
||||
</div>
|
||||
{{/if}}
|
||||
</li>
|
||||
102
app/assets/javascripts/admin/addon/components/webhook-event.js
Normal file
102
app/assets/javascripts/admin/addon/components/webhook-event.js
Normal file
@ -0,0 +1,102 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
|
||||
import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export default class WebhookEvent extends Component {
|
||||
@service dialog;
|
||||
|
||||
@tracked body = "";
|
||||
@tracked bodyLabel = "";
|
||||
@tracked expandDetails = null;
|
||||
@tracked headers = "";
|
||||
expandDetailsRequestKey = "request";
|
||||
expandDetailsResponseKey = "response";
|
||||
|
||||
get statusColorClasses() {
|
||||
const { status } = this.args.event;
|
||||
|
||||
if (!status) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (status >= 200 && status <= 299) {
|
||||
return "text-successful";
|
||||
} else {
|
||||
return "text-danger";
|
||||
}
|
||||
}
|
||||
|
||||
get createdAt() {
|
||||
return moment(this.args.event.created_at).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
get completion() {
|
||||
const seconds = Math.floor(this.args.event.duration / 10.0) / 100.0;
|
||||
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
|
||||
}
|
||||
|
||||
get expandRequestIcon() {
|
||||
return this.expandDetails === this.expandDetailsRequestKey
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
}
|
||||
|
||||
get expandResponseIcon() {
|
||||
return this.expandDetails === this.expandDetailsResponseKey
|
||||
? "ellipsis-h"
|
||||
: "ellipsis-v";
|
||||
}
|
||||
|
||||
@action
|
||||
redeliver() {
|
||||
return this.dialog.yesNoConfirm({
|
||||
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
|
||||
didConfirm: async () => {
|
||||
try {
|
||||
const json = await ajax(
|
||||
`/admin/api/web_hooks/${this.args.event.web_hook_id}/events/${this.args.event.id}/redeliver`,
|
||||
{ type: "POST" }
|
||||
);
|
||||
this.args.event.setProperties(json.web_hook_event);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
toggleRequest() {
|
||||
if (this.expandDetails !== this.expandDetailsRequestKey) {
|
||||
const headers = {
|
||||
"Request URL": this.args.event.request_url,
|
||||
"Request method": "POST",
|
||||
...ensureJSON(this.args.event.headers),
|
||||
};
|
||||
|
||||
this.headers = plainJSON(headers);
|
||||
this.body = prettyJSON(this.args.event.payload);
|
||||
this.expandDetails = this.expandDetailsRequestKey;
|
||||
this.bodyLabel = I18n.t("admin.web_hooks.events.payload");
|
||||
} else {
|
||||
this.expandDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleResponse() {
|
||||
if (this.expandDetails !== this.expandDetailsResponseKey) {
|
||||
this.headers = plainJSON(this.args.event.response_headers);
|
||||
this.body = this.args.event.response_body;
|
||||
this.expandDetails = this.expandDetailsResponseKey;
|
||||
this.bodyLabel = I18n.t("admin.web_hooks.events.body");
|
||||
} else {
|
||||
this.expandDetails = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,17 @@
|
||||
<div class="web-hook-direction">
|
||||
<LinkTo @route="adminWebHooks" class="btn">
|
||||
{{d-icon "list"}} {{i18n "admin.web_hooks.events.go_list"}}
|
||||
</LinkTo>
|
||||
<DButton @icon="paper-plane" @label="admin.web_hooks.events.ping" @action={{action "ping"}} @disabled={{this.pingDisabled}} />
|
||||
<LinkTo @route="adminWebHooks.show" @model={{this.model.extras.web_hook_id}} class="btn">
|
||||
{{d-icon "far-edit"}} {{i18n "admin.web_hooks.events.go_details"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
<div class="web-hook-events-listing"
|
||||
{{did-insert this.subscribe}}
|
||||
{{will-destroy this.unsubscribe}}
|
||||
>
|
||||
<DButton
|
||||
@icon="paper-plane"
|
||||
@label="admin.web_hooks.events.ping"
|
||||
@action={{this.ping}}
|
||||
@disabled={{not this.pingEnabled}}
|
||||
class="webhook-events__ping-button"
|
||||
/>
|
||||
|
||||
<div class="web-hook-events-listing">
|
||||
{{#if this.model}}
|
||||
<LoadMore @selector=".web-hook-events li" @action={{action "loadMore"}}>
|
||||
{{#if this.events}}
|
||||
<LoadMore @selector=".web-hook-events li" @action={{this.loadMore}}>
|
||||
<div class="web-hook-events content-list">
|
||||
<div class="heading-container">
|
||||
<div class="col heading first status">{{i18n "admin.web_hooks.events.status"}}</div>
|
||||
@ -18,20 +19,22 @@
|
||||
<div class="col heading timestamp">{{i18n "admin.web_hooks.events.timestamp"}}</div>
|
||||
<div class="col heading completion">{{i18n "admin.web_hooks.events.completion"}}</div>
|
||||
<div class="col heading actions">{{i18n "admin.web_hooks.events.actions"}}</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
{{#if this.hasIncoming}}
|
||||
<a href tabindex="0" {{on "click" this.showInserted}} class="alert alert-info clickable">
|
||||
<CountI18n @key="admin.web_hooks.events.incoming" @count={{this.incomingCount}} />
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<ul>
|
||||
{{#each this.model as |webHookEvent|}}
|
||||
<AdminWebHookEvent @model={{webHookEvent}} />
|
||||
{{#each this.events as |event|}}
|
||||
<WebhookEvent @event={{event}} />
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.events.loadingMore}} />
|
||||
</LoadMore>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.web_hooks.events.none"}}</p>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
{{d-icon this.iconName (hash class=this.iconClass)}}
|
||||
{{this.deliveryStatus}}
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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({});
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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));
|
||||
},
|
||||
|
||||
@ -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));
|
||||
},
|
||||
|
||||
@ -15,7 +15,7 @@ export default RestModel.extend({
|
||||
groupsFilterInName: null,
|
||||
|
||||
@discourseComputed("wildcard_web_hook")
|
||||
webHookType: {
|
||||
webhookType: {
|
||||
get(wildcard) {
|
||||
return wildcard ? "wildcard" : "individual";
|
||||
},
|
||||
|
||||
@ -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))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
@ -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" });
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Route from "@ember/routing/route";
|
||||
|
||||
export default Route.extend({
|
||||
model() {
|
||||
return this.store.findAll("web-hook");
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<div class="penalty-similar-users">
|
||||
<p class="alert alert-danger">
|
||||
{{i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username)}}
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{i18n "username"}}</th>
|
||||
<th>{{i18n "last_seen"}}</th>
|
||||
<th>{{i18n "admin.user.topics_entered"}}</th>
|
||||
<th>{{i18n "admin.user.posts_read_count"}}</th>
|
||||
<th>{{i18n "admin.user.time_read"}}</th>
|
||||
<th>{{i18n "created"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each this.user.similar_users as |user|}}
|
||||
<tr>
|
||||
<td>
|
||||
<Input @type="checkbox" disabled={{not (get user this.penaltyField)}} {{on "click" (action "selectUserId" user.id)}} />
|
||||
</td>
|
||||
<td>{{avatar user imageSize="small"}} {{user.username}}</td>
|
||||
<td>{{format-duration user.last_seen_age}}</td>
|
||||
<td>{{number user.topics_entered}}</td>
|
||||
<td>{{number user.posts_read_count}}</td>
|
||||
<td>{{format-duration user.time_read}}</td>
|
||||
<td>{{format-duration user.created_at_age}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -1,3 +0,0 @@
|
||||
<Input id={{this.typeName}} @type="checkbox" name="event-choice" @checked={{this.enabled}} />
|
||||
<label for={{this.typeName}}>{{this.name}}</label>
|
||||
<p>{{this.details}}</p>
|
||||
@ -1,19 +0,0 @@
|
||||
<div class="col first status">
|
||||
<span class={{this.statusColorClasses}}>{{this.model.status}}</span>
|
||||
</div>
|
||||
<div class="col event-id">{{this.model.id}}</div>
|
||||
<div class="col timestamp">{{this.createdAt}}</div>
|
||||
<div class="col completion">{{this.completion}}</div>
|
||||
<div class="col actions">
|
||||
<DButton @icon={{this.expandRequestIcon}} @action={{action "toggleRequest"}} @label="admin.web_hooks.events.request" />
|
||||
<DButton @icon={{this.expandResponseIcon}} @action={{action "toggleResponse"}} @label="admin.web_hooks.events.response" />
|
||||
<DButton @icon="sync" @action={{action "redeliver"}} @label="admin.web_hooks.events.redeliver" />
|
||||
</div>
|
||||
{{#if this.expandDetails}}
|
||||
<div class="details">
|
||||
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
|
||||
<pre><code>{{this.headers}}</code></pre>
|
||||
<h3>{{this.bodyLabel}}</h3>
|
||||
<pre><code>{{this.body}}</code></pre>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -1 +0,0 @@
|
||||
{{this.circleIcon}} {{this.deliveryStatus}}
|
||||
@ -173,11 +173,12 @@
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.model.component}}
|
||||
<DSection @class="form-horizontal theme settings control-unit">
|
||||
<DSection @class="form-horizontal theme settings control-unit theme-settings__color-scheme">
|
||||
<div class="row setting">
|
||||
<div class="setting-label">
|
||||
{{i18n "admin.customize.theme.color_scheme"}}
|
||||
</div>
|
||||
|
||||
<div class="setting-value">
|
||||
<ColorPalettes @content={{this.colorSchemes}} @value={{this.colorSchemeId}} @icon="paint-brush" @options={{hash
|
||||
filterable=true
|
||||
@ -185,6 +186,7 @@
|
||||
|
||||
<div class="desc">{{i18n "admin.customize.theme.color_scheme_select"}}</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-controls">
|
||||
{{#if this.colorSchemeChanged}}
|
||||
<DButton @action={{action "changeScheme"}} @class="ok submit-edit" @icon="check" />
|
||||
|
||||
@ -54,10 +54,8 @@
|
||||
</td>
|
||||
<td class="col last_match_at">
|
||||
{{#if item.last_match_at}}
|
||||
<div class="label">
|
||||
{{i18n "admin.logs.last_match_at"}}
|
||||
{{age-with-tooltip item.last_match_at}}
|
||||
</div>
|
||||
<div class="label">{{i18n "admin.logs.last_match_at"}}</div>
|
||||
{{age-with-tooltip item.last_match_at}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="col actions">
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.similar_users}}
|
||||
<AdminPenaltySimilarUsers @type="silence" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
|
||||
{{/if}}
|
||||
|
||||
</ConditionalLoadingSpinner>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
@ -13,12 +13,15 @@
|
||||
<FutureDateInput @class="suspend-until" @label="admin.user.suspend_duration" @clearable={{false}} @input={{this.suspendUntil}} @onChangeInput={{action (mut this.suspendUntil)}} />
|
||||
</label>
|
||||
</div>
|
||||
<SuspensionDetails @reason={{this.reason}} @message={{this.message}} />
|
||||
|
||||
<SuspensionDetails @reason={{this.reason}} @message={{this.message}} />
|
||||
{{#if this.postId}}
|
||||
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.similar_users}}
|
||||
<AdminPenaltySimilarUsers @type="suspend" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="cant-suspend">
|
||||
{{i18n "admin.user.cant_suspend"}}
|
||||
|
||||
122
app/assets/javascripts/admin/addon/templates/web-hooks-edit.hbs
Normal file
122
app/assets/javascripts/admin/addon/templates/web-hooks-edit.hbs
Normal file
@ -0,0 +1,122 @@
|
||||
<LinkTo @route="adminWebHooks" class="go-back">
|
||||
{{d-icon "arrow-left"}}
|
||||
{{i18n "admin.web_hooks.go_back"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="web-hook-container">
|
||||
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
|
||||
|
||||
<form class="web-hook form-horizontal">
|
||||
<div class="control-group">
|
||||
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
|
||||
<TextField @name="payload-url" @value={{this.model.payload_url}} @placeholderKey="admin.web_hooks.payload_url_placeholder" />
|
||||
<InputTip @validation={{this.urlValidation}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
|
||||
<ComboBox @content={{this.contentTypes}} @name="content-type" @value={{this.model.content_type}} @onChange={{action (mut this.model.content_type)}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
|
||||
<TextField @name="secret" @value={{this.model.secret}} @placeholderKey="admin.web_hooks.secret_placeholder" />
|
||||
<InputTip @validation={{this.secretValidation}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
|
||||
|
||||
<label class="subscription-choice">
|
||||
<RadioButton
|
||||
@name="subscription-choice"
|
||||
@value="individual"
|
||||
@selection={{this.model.webhookType}}
|
||||
/>
|
||||
{{i18n "admin.web_hooks.individual_event"}}
|
||||
<InputTip @validation={{this.eventTypeValidation}} />
|
||||
</label>
|
||||
|
||||
{{#unless this.model.wildcard_web_hook}}
|
||||
<div class="event-selector">
|
||||
{{#each this.eventTypes as |type|}}
|
||||
<WebhookEventChooser
|
||||
@type={{type}}
|
||||
@eventTypes={{this.model.web_hook_event_types}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<label class="subscription-choice">
|
||||
<RadioButton
|
||||
@name="subscription-choice"
|
||||
@value="wildcard"
|
||||
@selection={{this.model.webhookType}}
|
||||
/>
|
||||
{{i18n "admin.web_hooks.wildcard_event"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="filters control-group">
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.categories_filter"}}</label>
|
||||
<CategorySelector @categories={{this.model.categories}} @onChange={{action (mut this.model.categories)}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.categories_filter_instructions"}}</div>
|
||||
</div>
|
||||
|
||||
{{#if this.showTagsFilter}}
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.tags_filter"}}</label>
|
||||
<TagChooser @tags={{this.model.tag_names}} @everyTag={{true}} @excludeSynonyms={{true}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.tags_filter_instructions"}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.groups_filter"}}</label>
|
||||
<GroupSelector @groupNames={{this.model.groupsFilterInName}} @groupFinder={{this.model.groupFinder}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.groups_filter_instructions"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="web-hook-fields" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
|
||||
|
||||
<label>
|
||||
<Input @type="checkbox" name="verify_certificate" @checked={{this.model.verify_certificate}} />
|
||||
{{i18n "admin.web_hooks.verify_certificate"}}
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<Input @type="checkbox" name="active" @checked={{this.model.active}} />
|
||||
{{i18n "admin.web_hooks.active"}}
|
||||
</label>
|
||||
|
||||
{{#if this.model.active}}
|
||||
<div class="instructions">{{i18n "admin.web_hooks.active_notice"}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="controls">
|
||||
<DButton
|
||||
@translatedLabel={{this.saveButtonText}}
|
||||
@action={{action "save"}}
|
||||
@disabled={{this.saveButtonDisabled}}
|
||||
@class="btn-primary admin-webhooks__save-button"
|
||||
/>
|
||||
|
||||
{{#if this.model.isNew}}
|
||||
<LinkTo @route="adminWebHooks" class="btn btn-default">
|
||||
{{i18n "cancel"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo @route="adminWebHooks.show" @model={{this.model}} class="btn btn-default">
|
||||
{{i18n "cancel"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
<span class="saving">{{this.savingStatus}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,70 @@
|
||||
<div class="web-hooks-listing">
|
||||
<p>{{i18n "admin.web_hooks.instruction"}}</p>
|
||||
|
||||
<div class="new-webhook">
|
||||
<LinkTo
|
||||
@route="adminWebHooks.edit"
|
||||
@model="new"
|
||||
class="btn btn-default admin-webhooks__new-button"
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
{{i18n "admin.web_hooks.new"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
{{#if this.model}}
|
||||
<LoadMore @selector=".web-hooks tr" @action={{this.loadMore}}>
|
||||
<table class="web-hooks grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.description_label"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.controls"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |webhook|}}
|
||||
<tr>
|
||||
<td class="delivery-status">
|
||||
<LinkTo @route="adminWebHooks.show" @model={{webhook}}>
|
||||
<WebhookStatus
|
||||
@deliveryStatuses={{this.deliveryStatuses}}
|
||||
@webhook={{webhook}}
|
||||
/>
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="payload-url">
|
||||
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}>
|
||||
{{webhook.payload_url}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="description">{{webhook.description}}</td>
|
||||
<td class="controls">
|
||||
<LinkTo
|
||||
@route="adminWebHooks.edit"
|
||||
@model={{webhook}}
|
||||
class="btn btn-default no-text"
|
||||
title={{i18n "admin.web_hooks.edit"}}
|
||||
>
|
||||
{{d-icon "far-edit"}}
|
||||
</LinkTo>
|
||||
|
||||
<DButton
|
||||
@class="destroy btn-danger"
|
||||
@action={{this.destroy}}
|
||||
@actionParam={{webhook}}
|
||||
@icon="times"
|
||||
@title="delete"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
</LoadMore>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.web_hooks.none"}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -3,90 +3,32 @@
|
||||
{{i18n "admin.web_hooks.go_back"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="web-hook-container">
|
||||
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
|
||||
<form class="web-hook form-horizontal">
|
||||
<div class="control-group">
|
||||
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
|
||||
<TextField @name="payload-url" @value={{this.model.payload_url}} @placeholderKey="admin.web_hooks.payload_url_placeholder" />
|
||||
<InputTip @validation={{this.urlValidation}} />
|
||||
</div>
|
||||
<div class="admin-webhooks__summary">
|
||||
<h1>
|
||||
{{this.model.payload_url}}
|
||||
|
||||
<div class="control-group">
|
||||
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
|
||||
<ComboBox @content={{this.contentTypes}} @name="content-type" @value={{this.model.content_type}} @onChange={{action (mut this.model.content_type)}} />
|
||||
</div>
|
||||
<DButton
|
||||
@action={{this.edit}}
|
||||
@icon="far-edit"
|
||||
title={{i18n "admin.web_hooks.edit"}}
|
||||
class="btn no-text admin-webhooks__edit-button"
|
||||
/>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
|
||||
<TextField @name="secret" @value={{this.model.secret}} @placeholderKey="admin.web_hooks.secret_placeholder" />
|
||||
<InputTip @validation={{this.secretValidation}} />
|
||||
</div>
|
||||
<DButton
|
||||
@action={{this.destroy}}
|
||||
@icon="times"
|
||||
@title="delete"
|
||||
class="destroy btn-danger admin-webhooks__delete-button"
|
||||
/>
|
||||
</h1>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
|
||||
<div>
|
||||
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="individual" @selection={{this.model.webHookType}} />
|
||||
{{i18n "admin.web_hooks.individual_event"}}
|
||||
<InputTip @validation={{this.eventTypeValidation}} />
|
||||
</div>
|
||||
{{#unless this.model.wildcard_web_hook}}
|
||||
<div class="event-selector">
|
||||
{{#each this.eventTypes as |type|}}
|
||||
<AdminWebHookEventChooser @type={{type}} @model={{this.model.web_hook_event_types}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div>
|
||||
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="wildcard" @selection={{this.model.webHookType}} />
|
||||
{{i18n "admin.web_hooks.wildcard_event"}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="admin-webhooks__description-label">
|
||||
{{i18n "admin.web_hooks.description_label"}}:
|
||||
</span>
|
||||
|
||||
<div class="filters control-group">
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.categories_filter"}}</label>
|
||||
<CategorySelector @categories={{this.model.categories}} @onChange={{action (mut this.model.categories)}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.categories_filter_instructions"}}</div>
|
||||
</div>
|
||||
{{#if this.showTagsFilter}}
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.tags_filter"}}</label>
|
||||
<TagChooser @tags={{this.model.tag_names}} @everyTag={{true}} @excludeSynonyms={{true}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.tags_filter_instructions"}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n "admin.web_hooks.groups_filter"}}</label>
|
||||
<GroupSelector @groupNames={{this.model.groupsFilterInName}} @groupFinder={{this.model.groupFinder}} />
|
||||
<div class="instructions">{{i18n "admin.web_hooks.groups_filter_instructions"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="web-hook-fields" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
|
||||
|
||||
<div>
|
||||
<Input @type="checkbox" name="verify_certificate" @checked={{this.model.verify_certificate}} /> {{i18n "admin.web_hooks.verify_certificate"}}
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Input @type="checkbox" name="active" @checked={{this.model.active}} /> {{i18n "admin.web_hooks.active"}}
|
||||
</div>
|
||||
{{#if this.model.active}}
|
||||
<div class="instructions">{{i18n "admin.web_hooks.active_notice"}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="controls">
|
||||
<DButton @class="btn-default" @translatedLabel={{this.saveButtonText}} @action={{action "save"}} @disabled={{this.saveButtonDisabled}} />
|
||||
|
||||
{{#unless this.model.isNew}}
|
||||
<DButton @class="btn-danger" @label="admin.web_hooks.destroy" @action={{action "destroy"}} />
|
||||
<LinkTo @route="adminWebHooks.showEvents" @model={{this.model.id}} class="btn">
|
||||
{{i18n "admin.web_hooks.events.go_events"}}
|
||||
</LinkTo>
|
||||
{{/unless}}
|
||||
<span class="saving">{{this.savingStatus}}</span>
|
||||
{{this.model.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WebhookEvents @webhookId={{this.model.id}} />
|
||||
|
||||
@ -1,38 +1 @@
|
||||
<div class="web-hooks-listing">
|
||||
<p>{{i18n "admin.web_hooks.instruction"}}</p>
|
||||
<div class="new-webhook">
|
||||
<LinkTo @route="adminWebHooks.show" @model="new" class="btn btn-default">
|
||||
{{d-icon "plus"}} {{i18n "admin.web_hooks.new"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{#if this.model}}
|
||||
<LoadMore @selector=".web-hooks tr" @action={{action "loadMore"}}>
|
||||
<table class="web-hooks grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.description"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.controls"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |webHook|}}
|
||||
<tr>
|
||||
<td class="delivery-status"><LinkTo @route="adminWebHooks.showEvents" @model={{webHook.id}}><AdminWebHookStatus @deliveryStatuses={{this.deliveryStatuses}} @model={{webHook}} /></LinkTo></td>
|
||||
<td class="payload-url"><LinkTo @route="adminWebHooks.show" @model={{webHook}}>{{webHook.payload_url}}</LinkTo></td>
|
||||
<td class="description">{{webHook.description}}</td>
|
||||
<td class="controls">
|
||||
<LinkTo @route="adminWebHooks.show" @model={{webHook}} class="btn btn-default no-text">{{d-icon "far-edit"}}</LinkTo>
|
||||
<DButton @class="destroy btn-danger" @action={{action "destroy"}} @actionParam={{webHook}} @icon="times" />
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
</LoadMore>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.web_hooks.none"}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{outlet}}
|
||||
|
||||
@ -319,7 +319,13 @@ async function handleRequest(proxy, baseURL, req, res) {
|
||||
});
|
||||
|
||||
response.headers.forEach((value, header) => {
|
||||
res.set(header, value);
|
||||
if (header === "set-cookie") {
|
||||
// Special handling to get array of multiple Set-Cookie header values
|
||||
// per https://github.com/node-fetch/node-fetch/issues/251#issuecomment-428143940
|
||||
res.set("set-cookie", response.headers.raw()["set-cookie"]);
|
||||
} else {
|
||||
res.set(header, value);
|
||||
}
|
||||
});
|
||||
res.set("content-encoding", null);
|
||||
|
||||
|
||||
@ -10,9 +10,14 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.dialog.message}}
|
||||
{{#if (or this.dialog.message this.dialog.confirmPhrase)}}
|
||||
<div class="dialog-body">
|
||||
{{this.dialog.message}}
|
||||
{{#if this.dialog.message}}
|
||||
<p>{{this.dialog.message}}</p>
|
||||
{{/if}}
|
||||
{{#if this.dialog.confirmPhrase}}
|
||||
<TextField @value={{this.dialog.confirmPhraseInput}} {{on "input" this.dialog.onConfirmPhraseInput}} @id="confirm-phrase" @autocorrect="off" @autocapitalize="off" />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -21,9 +26,9 @@
|
||||
{{#each this.dialog.buttons as |button|}}
|
||||
<DButton @icon={{button.icon}} @class={{button.class}} @action={{action "handleButtonAction" button}} @translatedLabel={{button.label}} />
|
||||
{{else}}
|
||||
<DButton @class={{this.dialog.confirmButtonClass}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
|
||||
<DButton @class={{this.dialog.confirmButtonClass}} @disabled={{this.dialog.confirmButtonDisabled}} @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
|
||||
{{#if this.dialog.shouldDisplayCancel}}
|
||||
<DButton @class="btn-default" @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
|
||||
<DButton @class={{this.dialog.cancelButtonClass}} @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Service from "@ember/service";
|
||||
import A11yDialog from "a11y-dialog";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { isBlank } from "@ember/utils";
|
||||
|
||||
export default Service.extend({
|
||||
message: null,
|
||||
@ -13,7 +14,10 @@ export default Service.extend({
|
||||
confirmButtonIcon: null,
|
||||
confirmButtonLabel: null,
|
||||
confirmButtonClass: null,
|
||||
confirmPhrase: null,
|
||||
confirmPhraseInput: null,
|
||||
cancelButtonLabel: null,
|
||||
cancelButtonClass: null,
|
||||
shouldDisplayCancel: null,
|
||||
|
||||
didConfirm: null,
|
||||
@ -32,6 +36,8 @@ export default Service.extend({
|
||||
confirmButtonLabel = "ok_value",
|
||||
confirmButtonClass = "btn-primary",
|
||||
cancelButtonLabel = "cancel_value",
|
||||
cancelButtonClass = "btn-default",
|
||||
confirmPhrase,
|
||||
shouldDisplayCancel,
|
||||
|
||||
didConfirm,
|
||||
@ -39,6 +45,8 @@ export default Service.extend({
|
||||
buttons,
|
||||
} = params;
|
||||
|
||||
let confirmButtonDisabled = !isBlank(confirmPhrase);
|
||||
|
||||
const element = document.getElementById("dialog-holder");
|
||||
|
||||
this.setProperties({
|
||||
@ -49,10 +57,13 @@ export default Service.extend({
|
||||
title,
|
||||
titleElementId: title !== null ? "dialog-title" : null,
|
||||
|
||||
confirmButtonDisabled,
|
||||
confirmButtonClass,
|
||||
confirmButtonLabel,
|
||||
confirmButtonIcon,
|
||||
confirmPhrase,
|
||||
cancelButtonLabel,
|
||||
cancelButtonClass,
|
||||
shouldDisplayCancel,
|
||||
|
||||
didConfirm,
|
||||
@ -131,7 +142,10 @@ export default Service.extend({
|
||||
confirmButtonLabel: null,
|
||||
confirmButtonIcon: null,
|
||||
cancelButtonLabel: null,
|
||||
cancelButtonClass: null,
|
||||
shouldDisplayCancel: null,
|
||||
confirmPhrase: null,
|
||||
confirmPhraseInput: null,
|
||||
|
||||
didConfirm: null,
|
||||
didCancel: null,
|
||||
@ -160,4 +174,12 @@ export default Service.extend({
|
||||
cancel() {
|
||||
this.dialogInstance.hide();
|
||||
},
|
||||
|
||||
@bind
|
||||
onConfirmPhraseInput() {
|
||||
this.set(
|
||||
"confirmButtonDisabled",
|
||||
this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -65,16 +65,19 @@ export function enableMissingIconWarning() {
|
||||
}
|
||||
|
||||
export function renderIcon(renderType, id, params) {
|
||||
for (let i = 0; i < _renderers.length; i++) {
|
||||
let renderer = _renderers[i];
|
||||
let rendererForType = renderer[renderType];
|
||||
params ||= {};
|
||||
|
||||
if (rendererForType) {
|
||||
const icon = { id, replacementId: REPLACEMENTS[id] };
|
||||
let result = rendererForType(icon, params || {});
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
for (const renderer of _renderers) {
|
||||
const rendererForType = renderer[renderType];
|
||||
if (!rendererForType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const icon = { id, replacementId: REPLACEMENTS[id] };
|
||||
const result = rendererForType(icon, params);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
"start": "ember serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"ember-auto-import": "^2.5.0",
|
||||
"ember-cli-babel": "^7.26.10",
|
||||
"ember-cli-htmlbars": "^6.1.1",
|
||||
|
||||
@ -3,12 +3,12 @@ const ColocatedTemplateProcessor = require("ember-cli-htmlbars/lib/colocated-bro
|
||||
module.exports = class DiscoursePluginColocatedTemplateProcessor extends (
|
||||
ColocatedTemplateProcessor
|
||||
) {
|
||||
constructor(tree, discoursePluginName) {
|
||||
constructor(tree, rootName) {
|
||||
super(tree);
|
||||
this.discoursePluginName = discoursePluginName;
|
||||
this.rootName = rootName;
|
||||
}
|
||||
|
||||
detectRootName() {
|
||||
return `discourse/plugins/${this.discoursePluginName}/discourse`;
|
||||
return this.rootName;
|
||||
}
|
||||
};
|
||||
|
||||
@ -176,7 +176,15 @@ module.exports = {
|
||||
|
||||
tree = RawHandlebarsCompiler(tree);
|
||||
|
||||
tree = new DiscoursePluginColocatedTemplateProcessor(tree, pluginName);
|
||||
const colocateBase = `discourse/plugins/${pluginName}`;
|
||||
tree = new DiscoursePluginColocatedTemplateProcessor(
|
||||
tree,
|
||||
`${colocateBase}/discourse`
|
||||
);
|
||||
tree = new DiscoursePluginColocatedTemplateProcessor(
|
||||
tree,
|
||||
`${colocateBase}/admin`
|
||||
);
|
||||
tree = this.compileTemplates(tree);
|
||||
|
||||
tree = this.processedAddonJsFiles(tree);
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { alias, not } from "@ember/object/computed";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import discourseComputed, {
|
||||
bind,
|
||||
observes,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
|
||||
export default Component.extend({
|
||||
@ -40,18 +43,11 @@ export default Component.extend({
|
||||
this._super(...arguments);
|
||||
|
||||
this.topics.forEach((topic) => {
|
||||
const includeUnreadIndicator =
|
||||
typeof topic.unread_by_group_member !== "undefined";
|
||||
|
||||
if (includeUnreadIndicator) {
|
||||
const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
|
||||
this.messageBus.subscribe(unreadIndicatorChannel, (data) => {
|
||||
const nodeClassList = document.querySelector(
|
||||
`.indicator-topic-${data.topic_id}`
|
||||
).classList;
|
||||
|
||||
nodeClassList.toggle("read", !data.show_indicator);
|
||||
});
|
||||
if (typeof topic.unread_by_group_member !== "undefined") {
|
||||
this.messageBus.subscribe(
|
||||
`/private-messages/unread-indicator/${topic.id}`,
|
||||
this.onMessage
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -59,15 +55,19 @@ export default Component.extend({
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.topics.forEach((topic) => {
|
||||
const includeUnreadIndicator =
|
||||
typeof topic.unread_by_group_member !== "undefined";
|
||||
this.messageBus.unsubscribe(
|
||||
"/private-messages/unread-indicator/*",
|
||||
this.onMessage
|
||||
);
|
||||
},
|
||||
|
||||
if (includeUnreadIndicator) {
|
||||
const unreadIndicatorChannel = `/private-messages/unread-indicator/${topic.id}`;
|
||||
this.messageBus.unsubscribe(unreadIndicatorChannel);
|
||||
}
|
||||
});
|
||||
@bind
|
||||
onMessage(data) {
|
||||
const nodeClassList = document.querySelector(
|
||||
`.indicator-topic-${data.topic_id}`
|
||||
).classList;
|
||||
|
||||
nodeClassList.toggle("read", !data.show_indicator);
|
||||
},
|
||||
|
||||
@discourseComputed("topics")
|
||||
|
||||
@ -41,7 +41,7 @@ export default class BookmarkIcon extends Component {
|
||||
if (!isEmpty(this.bookmark.reminder_at)) {
|
||||
const formattedTime = formattedReminderTime(
|
||||
this.bookmark.reminder_at,
|
||||
this.currentUser.timezone
|
||||
this.currentUser.user_option.timezone
|
||||
);
|
||||
return I18n.t("bookmarks.created_with_reminder_generic", {
|
||||
date: formattedTime,
|
||||
|
||||
@ -57,7 +57,7 @@ export default Component.extend({
|
||||
postDetectedLocalTime: null,
|
||||
postDetectedLocalTimezone: null,
|
||||
prefilledDatetime: null,
|
||||
userTimezone: this.currentUser.timezone,
|
||||
userTimezone: this.currentUser.user_option.timezone,
|
||||
showOptions: false,
|
||||
_itsatrap: new ItsATrap(),
|
||||
autoDeletePreference: this.model.autoDeletePreference || 0,
|
||||
@ -154,7 +154,7 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
this.currentUser.set(
|
||||
"bookmark_auto_delete_preference",
|
||||
"user_option.bookmark_auto_delete_preference",
|
||||
this.autoDeletePreference
|
||||
);
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ import {
|
||||
linkSeenHashtagsInContext,
|
||||
} from "discourse/lib/hashtag-autocomplete";
|
||||
import {
|
||||
cannotSee,
|
||||
fetchUnseenMentions,
|
||||
linkSeenMentions,
|
||||
} from "discourse/lib/link-mentions";
|
||||
@ -126,6 +125,7 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.warnedCannotSeeMentions = [];
|
||||
this.warnedGroupMentions = [];
|
||||
},
|
||||
|
||||
@discourseComputed("composer.requiredCategoryMissing")
|
||||
@ -474,13 +474,15 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
},
|
||||
|
||||
_renderUnseenMentions(preview, unseen) {
|
||||
// 'Create a New Topic' scenario is not supported (per conversation with codinghorror)
|
||||
// https://meta.discourse.org/t/taking-another-1-7-release-task/51986/7
|
||||
fetchUnseenMentions(unseen, this.get("composer.topic.id")).then((r) => {
|
||||
fetchUnseenMentions({
|
||||
names: unseen,
|
||||
topicId: this.get("composer.topic.id"),
|
||||
allowedNames: this.get("composer.targetRecipients")?.split(","),
|
||||
}).then((response) => {
|
||||
linkSeenMentions(preview, this.siteSettings);
|
||||
this._warnMentionedGroups(preview);
|
||||
this._warnCannotSeeMention(preview);
|
||||
this._warnHereMention(r.here_count);
|
||||
this._warnHereMention(response.here_count);
|
||||
});
|
||||
},
|
||||
|
||||
@ -506,28 +508,27 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
}
|
||||
},
|
||||
|
||||
@debounce(2000)
|
||||
_warnMentionedGroups(preview) {
|
||||
schedule("afterRender", () => {
|
||||
let found = this.warnedGroupMentions || [];
|
||||
preview?.querySelectorAll(".mention-group.notify")?.forEach((mention) => {
|
||||
if (this._isInQuote(mention)) {
|
||||
return;
|
||||
}
|
||||
preview
|
||||
.querySelectorAll(".mention-group[data-mentionable-user-count]")
|
||||
.forEach((mention) => {
|
||||
const { name } = mention.dataset;
|
||||
if (
|
||||
this.warnedGroupMentions.includes(name) ||
|
||||
this._isInQuote(mention)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = mention.dataset.name;
|
||||
if (!found.includes(name)) {
|
||||
this.groupsMentioned([
|
||||
{
|
||||
name,
|
||||
user_count: mention.dataset.mentionableUserCount,
|
||||
max_mentions: mention.dataset.maxMentions,
|
||||
},
|
||||
]);
|
||||
found.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
this.set("warnedGroupMentions", found);
|
||||
this.warnedGroupMentions.push(name);
|
||||
this.groupsMentioned({
|
||||
name,
|
||||
userCount: mention.dataset.mentionableUserCount,
|
||||
maxMentions: mention.dataset.maxMentions,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -539,22 +540,35 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
return;
|
||||
}
|
||||
|
||||
const warnings = [];
|
||||
|
||||
preview.querySelectorAll(".mention.cannot-see").forEach((mention) => {
|
||||
preview.querySelectorAll(".mention[data-reason]").forEach((mention) => {
|
||||
const { name } = mention.dataset;
|
||||
|
||||
if (this.warnedCannotSeeMentions.includes(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.warnedCannotSeeMentions.push(name);
|
||||
warnings.push({ name, reason: cannotSee[name] });
|
||||
this.cannotSeeMention({
|
||||
name,
|
||||
reason: mention.dataset.reason,
|
||||
});
|
||||
});
|
||||
|
||||
if (warnings.length > 0) {
|
||||
this.cannotSeeMention(warnings);
|
||||
}
|
||||
preview
|
||||
.querySelectorAll(".mention-group[data-reason]")
|
||||
.forEach((mention) => {
|
||||
const { name } = mention.dataset;
|
||||
if (this.warnedCannotSeeMentions.includes(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.warnedCannotSeeMentions.push(name);
|
||||
this.cannotSeeMention({
|
||||
name,
|
||||
reason: mention.dataset.reason,
|
||||
notifiedCount: mention.dataset.notifiedUserCount,
|
||||
isGroup: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_warnHereMention(hereCount) {
|
||||
@ -562,13 +576,7 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
return;
|
||||
}
|
||||
|
||||
discourseLater(
|
||||
this,
|
||||
() => {
|
||||
this.hereMention(hereCount);
|
||||
},
|
||||
2000
|
||||
);
|
||||
this.hereMention(hereCount);
|
||||
},
|
||||
|
||||
@bind
|
||||
|
||||
@ -464,9 +464,11 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
this.site.hashtag_configurations["topic-composer"],
|
||||
this._$textarea,
|
||||
this.siteSettings,
|
||||
(value) => {
|
||||
this.set("value", value);
|
||||
schedule("afterRender", this, this.focusTextArea);
|
||||
{
|
||||
afterComplete: (value) => {
|
||||
this.set("value", value);
|
||||
schedule("afterRender", this, this.focusTextArea);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@ -84,7 +84,7 @@ export default Component.extend({
|
||||
|
||||
@discourseComputed()
|
||||
timeOptions() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
|
||||
return [
|
||||
|
||||
@ -28,7 +28,7 @@ export default Component.extend({
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.userTimezone = this.currentUser.timezone;
|
||||
this.userTimezone = this.currentUser.user_option.timezone;
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
@jumpBottom={{@jumpBottom}}
|
||||
@jumpEnd={{@jumpEnd}}
|
||||
@jumpToIndex={{@jumpToIndex}}
|
||||
@jumpToPostPrompt={{@jumpToPostPrompt}}
|
||||
@fullscreen={{@fullscreen}}
|
||||
@mobileView={{@mobileView}}
|
||||
@currentUser={{this.currentUser}}
|
||||
@ -23,50 +24,7 @@
|
||||
@resetBumpDate={{@resetBumpDate}}
|
||||
@convertToPublicTopic={{@convertToPublicTopic}}
|
||||
@convertToPrivateMessage={{@convertToPrivateMessage}}
|
||||
@replyToPost={{@replyToPost}}
|
||||
/>
|
||||
|
||||
<div class="timeline-footer-controls">
|
||||
{{#if this.displaySummary}}
|
||||
<button type="button" class="show-summary btn-small" label={{i18n "summary.short_label"}} title={{i18n "summary.short_title"}} {{on "click" @showSummary}}>
|
||||
{{d-icon "layer-group"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.currentUser (not @fullscreen))}}
|
||||
{{#if this.canCreatePost}}
|
||||
<button type="button" class="btn btn-default create reply-to-post no-text btn-icon" title={{i18n "topic.reply.help"}} {{on "click" (fn @replyToPost null)}}>
|
||||
{{d-icon "reply"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @fullscreen}}
|
||||
<button
|
||||
type="button"
|
||||
{{!-- we need to keep this a widget-button to not close the modal when opening form --}}
|
||||
class="widget-button btn btn-text jump-to-post"
|
||||
title={{i18n "topic.progress.jump_prompt_long"}}
|
||||
{{on "click" @jumpToPostPrompt}}
|
||||
>
|
||||
<span class="d-button-label">
|
||||
{{i18n "topic.progress.jump_prompt"}}
|
||||
</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.currentUser}}
|
||||
<TopicNotificationsButton
|
||||
@notificationLevel={{@model.details.notification_level}}
|
||||
@topic={{@model}}
|
||||
@showFullTitle={{false}}
|
||||
@appendReason={{false}}
|
||||
@placement={{"bottom-end"}}
|
||||
@showCaret={{false}}
|
||||
/>
|
||||
{{#if @mobileView}}
|
||||
<TopicAdminMenuButton @topic={{@model}} @addKeyboardTargetClass={{true}} @openUpwards={{true}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,10 +79,6 @@ export default class GlimmerTopicTimeline extends Component {
|
||||
return this.args.fullscreen && !this.args.addShowClass;
|
||||
}
|
||||
|
||||
get canCreatePost() {
|
||||
return this.args.model.details?.can_create_post;
|
||||
}
|
||||
|
||||
get createdAt() {
|
||||
return new Date(this.args.model.created_at);
|
||||
}
|
||||
|
||||
@ -424,7 +424,7 @@ export default Component.extend(KeyEnterEscape, {
|
||||
embedQuoteButton(canCreatePost, canReplyAsNewTopic) {
|
||||
return (
|
||||
(canCreatePost || canReplyAsNewTopic) &&
|
||||
this.currentUser?.get("enable_quoting")
|
||||
this.currentUser?.get("user_option.enable_quoting")
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -42,12 +42,10 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod")
|
||||
showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) {
|
||||
@discourseComputed("backupEnabled", "secondFactorMethod")
|
||||
showToggleMethodLink(backupEnabled, secondFactorMethod) {
|
||||
return (
|
||||
backupEnabled &&
|
||||
totpEnabled &&
|
||||
secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -42,10 +42,9 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
|
||||
@observes("site.narrowDesktopView")
|
||||
narrowDesktopViewChanged() {
|
||||
if (
|
||||
this.siteSettings.enable_experimental_sidebar_hamburger &&
|
||||
(!this.sidebarEnabled || this.site.narrowDesktopView)
|
||||
) {
|
||||
this.eventDispatched("dom:clean", "header");
|
||||
|
||||
if (this._dropDownHeaderEnabled()) {
|
||||
this.appEvents.on(
|
||||
"sidebar-hamburger-dropdown:rendered",
|
||||
this,
|
||||
@ -231,10 +230,7 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
this.appEvents.on("user-menu:rendered", this, "_animateMenu");
|
||||
}
|
||||
|
||||
if (
|
||||
this.siteSettings.enable_experimental_sidebar_hamburger &&
|
||||
(!this.sidebarEnabled || this.site.narrowDesktopView)
|
||||
) {
|
||||
if (this._dropDownHeaderEnabled()) {
|
||||
this.appEvents.on(
|
||||
"sidebar-hamburger-dropdown:rendered",
|
||||
this,
|
||||
@ -323,10 +319,7 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
this.appEvents.off("user-menu:rendered", this, "_animateMenu");
|
||||
}
|
||||
|
||||
if (
|
||||
this.siteSettings.enable_experimental_sidebar_hamburger &&
|
||||
!this.sidebarEnabled
|
||||
) {
|
||||
if (this._dropDownHeaderEnabled()) {
|
||||
this.appEvents.off(
|
||||
"sidebar-hamburger-dropdown:rendered",
|
||||
this,
|
||||
@ -468,6 +461,14 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
this._animate = false;
|
||||
});
|
||||
},
|
||||
|
||||
_dropDownHeaderEnabled() {
|
||||
return (
|
||||
(!this.sidebarEnabled &&
|
||||
this.siteSettings.navigation_menu !== "legacy") ||
|
||||
this.site.narrowDesktopView
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import discourseComputed, { on } from "discourse-common/utils/decorators";
|
||||
import discourseComputed, { bind, on } from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
@ -13,36 +13,49 @@ export default Component.extend({
|
||||
animatePrompt: false,
|
||||
_timeoutHandler: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.messageBus.subscribe("/refresh_client", this.onRefresh);
|
||||
this.messageBus.subscribe("/global/asset-version", this.onAsset);
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.messageBus.unsubscribe("/refresh_client", this.onRefresh);
|
||||
this.messageBus.unsubscribe("/global/asset-version", this.onAsset);
|
||||
},
|
||||
|
||||
@bind
|
||||
onRefresh() {
|
||||
this.session.requiresRefresh = true;
|
||||
},
|
||||
|
||||
@bind
|
||||
onAsset(version) {
|
||||
if (this.session.assetVersion !== version) {
|
||||
this.session.requiresRefresh = true;
|
||||
}
|
||||
|
||||
if (!this._timeoutHandler && this.session.requiresRefresh) {
|
||||
if (isTesting()) {
|
||||
this.updatePromptState(true);
|
||||
} else {
|
||||
// Since we can do this transparently for people browsing the forum
|
||||
// hold back the message 24 hours.
|
||||
this._timeoutHandler = discourseLater(() => {
|
||||
this.updatePromptState(true);
|
||||
}, 1000 * 60 * 24 * 60);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
rootUrl() {
|
||||
return getURL("/");
|
||||
},
|
||||
|
||||
@on("init")
|
||||
initSubscriptions() {
|
||||
this.messageBus.subscribe("/refresh_client", () => {
|
||||
this.session.requiresRefresh = true;
|
||||
});
|
||||
|
||||
this.messageBus.subscribe("/global/asset-version", (version) => {
|
||||
if (this.session.assetVersion !== version) {
|
||||
this.session.requiresRefresh = true;
|
||||
}
|
||||
|
||||
if (!this._timeoutHandler && this.session.requiresRefresh) {
|
||||
if (isTesting()) {
|
||||
this.updatePromptState(true);
|
||||
} else {
|
||||
// Since we can do this transparently for people browsing the forum
|
||||
// hold back the message 24 hours.
|
||||
this._timeoutHandler = discourseLater(() => {
|
||||
this.updatePromptState(true);
|
||||
}, 1000 * 60 * 24 * 60);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updatePromptState(value) {
|
||||
// when adding the message, we inject the HTML then add the animation
|
||||
// when dismissing, things need to happen in the opposite order
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { computed, get } from "@ember/object";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import Site from "discourse/models/site";
|
||||
import { categoryBadgeHTML } from "discourse/helpers/category-link";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
@ -51,7 +50,7 @@ export default Component.extend({
|
||||
|
||||
if (suggestedGroupName) {
|
||||
return I18n.messageFormat("user.messages.read_more_group_pm_MF", {
|
||||
BOTH: hasBoth,
|
||||
HAS_UNREAD_AND_NEW: hasBoth,
|
||||
UNREAD: unreadCount,
|
||||
NEW: newCount,
|
||||
username,
|
||||
@ -61,7 +60,7 @@ export default Component.extend({
|
||||
});
|
||||
} else {
|
||||
return I18n.messageFormat("user.messages.read_more_personal_pm_MF", {
|
||||
BOTH: hasBoth,
|
||||
HAS_UNREAD_AND_NEW: hasBoth,
|
||||
UNREAD: unreadCount,
|
||||
NEW: newCount,
|
||||
username,
|
||||
@ -81,31 +80,15 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
_topicBrowseMoreMessage(topic) {
|
||||
const opts = {
|
||||
latestLink: `<a href="${getURL("/latest")}">${I18n.t(
|
||||
"topic.view_latest_topics"
|
||||
)}</a>`,
|
||||
};
|
||||
let category = topic.get("category");
|
||||
|
||||
if (
|
||||
category &&
|
||||
get(category, "id") === Site.currentProp("uncategorized_category_id")
|
||||
get(category, "id") === this.site.uncategorized_category_id
|
||||
) {
|
||||
category = null;
|
||||
}
|
||||
|
||||
if (category) {
|
||||
opts.catLink = categoryBadgeHTML(category);
|
||||
} else {
|
||||
opts.catLink =
|
||||
'<a href="' +
|
||||
getURL("/categories") +
|
||||
'">' +
|
||||
I18n.t("topic.browse_all_categories") +
|
||||
"</a>";
|
||||
}
|
||||
|
||||
let unreadTopics = 0;
|
||||
let newTopics = 0;
|
||||
|
||||
@ -115,21 +98,24 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
if (newTopics + unreadTopics > 0) {
|
||||
const hasBoth = unreadTopics > 0 && newTopics > 0;
|
||||
|
||||
return I18n.messageFormat("topic.read_more_MF", {
|
||||
BOTH: hasBoth,
|
||||
HAS_UNREAD_AND_NEW: unreadTopics > 0 && newTopics > 0,
|
||||
UNREAD: unreadTopics,
|
||||
NEW: newTopics,
|
||||
CATEGORY: category ? true : false,
|
||||
latestLink: opts.latestLink,
|
||||
catLink: opts.catLink,
|
||||
HAS_CATEGORY: category ? true : false,
|
||||
categoryLink: category ? categoryBadgeHTML(category) : null,
|
||||
basePath: getURL(""),
|
||||
});
|
||||
} else if (category) {
|
||||
return I18n.t("topic.read_more_in_category", opts);
|
||||
return I18n.t("topic.read_more_in_category", {
|
||||
categoryLink: categoryBadgeHTML(category),
|
||||
latestLink: getURL("/latest"),
|
||||
});
|
||||
} else {
|
||||
return I18n.t("topic.read_more", opts);
|
||||
return I18n.t("topic.read_more", {
|
||||
categoryLink: getURL("/categories"),
|
||||
latestLink: getURL("/latest"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ export default Component.extend({
|
||||
@on("init")
|
||||
_setupPicker() {
|
||||
this.setProperties({
|
||||
userTimezone: this.currentUser.timezone,
|
||||
userTimezone: this.currentUser.user_option.timezone,
|
||||
hiddenOptions: this.hiddenOptions || [],
|
||||
customOptions: this.customOptions || [],
|
||||
customLabels: this.customLabels || {},
|
||||
|
||||
@ -56,7 +56,7 @@ export default Component.extend({
|
||||
|
||||
canInviteTo: alias("topic.details.can_invite_to"),
|
||||
|
||||
canDefer: alias("currentUser.enable_defer"),
|
||||
canDefer: alias("currentUser.user_option.enable_defer"),
|
||||
|
||||
inviteDisabled: or("topic.archived", "topic.closed", "topic.deleted"),
|
||||
|
||||
|
||||
@ -77,13 +77,7 @@ export default Component.extend({
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.includeUnreadIndicator) {
|
||||
this.messageBus.subscribe(this.unreadIndicatorChannel, (data) => {
|
||||
const nodeClassList = document.querySelector(
|
||||
`.indicator-topic-${data.topic_id}`
|
||||
).classList;
|
||||
|
||||
nodeClassList.toggle("read", !data.show_indicator);
|
||||
});
|
||||
this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
|
||||
}
|
||||
|
||||
schedule("afterRender", () => {
|
||||
@ -101,9 +95,8 @@ export default Component.extend({
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.includeUnreadIndicator) {
|
||||
this.messageBus.unsubscribe(this.unreadIndicatorChannel);
|
||||
}
|
||||
this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
|
||||
|
||||
if (this._shouldFocusLastVisited()) {
|
||||
const title = this._titleElement();
|
||||
if (title) {
|
||||
@ -113,6 +106,15 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@bind
|
||||
onMessage(data) {
|
||||
const nodeClassList = document.querySelector(
|
||||
`.indicator-topic-${data.topic_id}`
|
||||
).classList;
|
||||
|
||||
nodeClassList.toggle("read", !data.show_indicator);
|
||||
},
|
||||
|
||||
@discourseComputed("topic.id")
|
||||
unreadIndicatorChannel(topicId) {
|
||||
return `/private-messages/unread-indicator/${topicId}`;
|
||||
|
||||
@ -79,7 +79,7 @@ export default Component.extend({
|
||||
: this._reset("invisible");
|
||||
},
|
||||
|
||||
_set(name, icon, key, iconArgs = null) {
|
||||
_set(name, icon, key, iconArgs) {
|
||||
this.set(`${name}Icon`, htmlSafe(iconHTML(`${icon}`, iconArgs)));
|
||||
this.set(`${name}Title`, I18n.t(`topic_statuses.${key}.help`));
|
||||
return true;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{{#if @fullscreen}}
|
||||
<div class="title">
|
||||
<h2>
|
||||
<a class="fancy-title" href {{on "click" @jumpTop}}>{{if @mobileView @model.fancyTitle ""}}</a>
|
||||
<a class="fancy-title" href {{on "click" @jumpTop}}>{{this.topicTitle}}</a>
|
||||
</h2>
|
||||
{{#if (or this.siteSettings.topic_featured_link_enabled this.showTags)}}
|
||||
<div class="topic-header-extra">
|
||||
@ -94,4 +94,48 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-footer-controls">
|
||||
{{#if this.displaySummary}}
|
||||
<button type="button" class="show-summary btn-small" label={{i18n "summary.short_label"}} title={{i18n "summary.short_title"}} {{on "click" @showSummary}}>
|
||||
{{d-icon "layer-group"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and @currentUser (not @fullscreen))}}
|
||||
{{#if this.canCreatePost}}
|
||||
<button type="button" class="btn btn-default create reply-to-post no-text btn-icon" title={{i18n "topic.reply.help"}} {{on "click" (fn @replyToPost null)}}>
|
||||
{{d-icon "reply"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if @fullscreen}}
|
||||
<button
|
||||
type="button"
|
||||
{{!-- we need to keep this a widget-button to not close the modal when opening form --}}
|
||||
class="widget-button btn btn-text jump-to-post"
|
||||
title={{i18n "topic.progress.jump_prompt_long"}}
|
||||
{{on "click" @jumpToPostPrompt}}
|
||||
>
|
||||
<span class="d-button-label">
|
||||
{{i18n "topic.progress.jump_prompt"}}
|
||||
</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if @currentUser}}
|
||||
<TopicNotificationsButton
|
||||
@notificationLevel={{@model.details.notification_level}}
|
||||
@topic={{@model}}
|
||||
@showFullTitle={{false}}
|
||||
@appendReason={{false}}
|
||||
@placement={{"bottom-end"}}
|
||||
@showCaret={{false}}
|
||||
/>
|
||||
{{#if @mobileView}}
|
||||
<TopicAdminMenuButton @topic={{@model}} @addKeyboardTargetClass={{true}} @openUpwards={{true}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -27,7 +27,6 @@ export default class TopicTimelineScrollArea extends Component {
|
||||
@tracked total;
|
||||
@tracked date;
|
||||
@tracked lastReadPercentage = null;
|
||||
@tracked displayTimeLineScrollArea = true;
|
||||
@tracked before;
|
||||
@tracked after;
|
||||
@tracked timelineScrollareaStyle;
|
||||
@ -38,15 +37,6 @@ export default class TopicTimelineScrollArea extends Component {
|
||||
super(...arguments);
|
||||
|
||||
if (!this.args.mobileView) {
|
||||
const streamLength = this.args.model.postStream?.stream?.length;
|
||||
|
||||
if (streamLength === 1) {
|
||||
const postsWrapper = document.querySelector(".posts-wrapper");
|
||||
if (postsWrapper && postsWrapper.offsetHeight < 1000) {
|
||||
this.displayTimeLineScrollArea = false;
|
||||
}
|
||||
}
|
||||
|
||||
// listen for scrolling event to update timeline
|
||||
this.appEvents.on("topic:current-post-scrolled", this.postScrolled);
|
||||
// listen for composer sizing changes to update timeline
|
||||
@ -58,6 +48,30 @@ export default class TopicTimelineScrollArea extends Component {
|
||||
this.calculatePosition();
|
||||
}
|
||||
|
||||
get displayTimeLineScrollArea() {
|
||||
if (this.args.mobileView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const streamLength = this.args.model.postStream?.stream?.length;
|
||||
if (streamLength === 1) {
|
||||
const postsWrapper = document.querySelector(".posts-wrapper");
|
||||
if (postsWrapper && postsWrapper.offsetHeight < 1000) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get canCreatePost() {
|
||||
return this.args.model.details?.can_create_post;
|
||||
}
|
||||
|
||||
get topicTitle() {
|
||||
return htmlSafe(this.args.mobileView ? this.args.model.fancyTitle : "");
|
||||
}
|
||||
|
||||
get showTags() {
|
||||
return (
|
||||
this.siteSettings.tagging_enabled && this.args.model.tags?.length > 0
|
||||
|
||||
@ -92,7 +92,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||
if (!this.showUserLocalTime) {
|
||||
return;
|
||||
}
|
||||
return user.timezone;
|
||||
return user.get("user_option.timezone");
|
||||
},
|
||||
|
||||
@discourseComputed("userTimezone")
|
||||
|
||||
@ -83,7 +83,7 @@ const CORE_TOP_TABS = [
|
||||
}
|
||||
|
||||
get shouldDisplay() {
|
||||
return !this.currentUser.likes_notifications_disabled;
|
||||
return !this.currentUser.user_option.likes_notifications_disabled;
|
||||
}
|
||||
|
||||
get count() {
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
</LinkTo>
|
||||
</li>
|
||||
|
||||
{{#if @siteSettings.enable_experimental_sidebar_hamburger}}
|
||||
{{#if (not (eq @siteSettings.navigation_menu "legacy"))}}
|
||||
<li class="indent nav-sidebar">
|
||||
<LinkTo @route="preferences.sidebar">
|
||||
{{d-icon "bars"}}
|
||||
|
||||
@ -12,10 +12,10 @@ export default class UserStatusMessage extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
return until(
|
||||
this.status.ends_at,
|
||||
this.currentUser.timezone,
|
||||
this.currentUser.locale
|
||||
);
|
||||
const timezone = this.currentUser
|
||||
? this.currentUser.timezone
|
||||
: moment.tz.guess();
|
||||
|
||||
return until(this.status.ends_at, timezone, this.currentUser?.locale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
{{#if this.editing}}
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<Input
|
||||
{{on "input" this.onInput}}
|
||||
@value={{this.newUsername}}
|
||||
maxlength={{this.maxLength}}
|
||||
class="input-xxlarge username-preference__input"
|
||||
/>
|
||||
|
||||
<div class="instructions">
|
||||
<p>
|
||||
{{#if this.taken}}
|
||||
{{i18n "user.change_username.taken"}}
|
||||
{{/if}}
|
||||
<span>{{this.errorMessage}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<DButton
|
||||
@action={{this.changeUsername}}
|
||||
@type="submit"
|
||||
@disabled={{this.saveDisabled}}
|
||||
@translatedLabel={{this.saveButtonText}}
|
||||
class="btn-primary username-preference__submit"
|
||||
/>
|
||||
|
||||
<DModalCancel @close={{this.toggleEditing}} />
|
||||
|
||||
{{#if this.saved}}{{i18n "saved"}}{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="controls">
|
||||
<span class="static username-preference__current-username">{{@user.username}}</span>
|
||||
|
||||
{{#if @user.can_edit_username}}
|
||||
<DButton
|
||||
@action={{this.toggleEditing}}
|
||||
@actionParam={{@user}}
|
||||
@icon="pencil-alt"
|
||||
@title="user.username.edit"
|
||||
class="btn-small username-preference__edit-username"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.siteSettings.enable_mentions}}
|
||||
<div class="instructions">
|
||||
{{html-safe (i18n "user.username.short_instructions" username=@user.username)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@ -0,0 +1,100 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import { empty, or } from "@ember/object/computed";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import I18n from "I18n";
|
||||
import User from "discourse/models/user";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class UsernamePreference extends Component {
|
||||
@service siteSettings;
|
||||
@service dialog;
|
||||
|
||||
@tracked editing = false;
|
||||
@tracked newUsername = this.args.user.username;
|
||||
@tracked errorMessage = null;
|
||||
@tracked saving = false;
|
||||
@tracked taken = false;
|
||||
|
||||
@setting("max_username_length") maxLength;
|
||||
@setting("min_username_length") minLength;
|
||||
@empty("newUsername") newUsernameEmpty;
|
||||
|
||||
@or("saving", "newUsernameEmpty", "taken", "unchanged", "errorMessage")
|
||||
saveDisabled;
|
||||
|
||||
get unchanged() {
|
||||
return this.newUsername === this.args.user.username;
|
||||
}
|
||||
|
||||
get saveButtonText() {
|
||||
return this.saving ? I18n.t("saving") : I18n.t("user.change");
|
||||
}
|
||||
|
||||
@action
|
||||
toggleEditing() {
|
||||
this.editing = !this.editing;
|
||||
|
||||
this.newUsername = this.args.user.username;
|
||||
this.errorMessage = null;
|
||||
this.saving = false;
|
||||
this.taken = false;
|
||||
}
|
||||
|
||||
@action
|
||||
async onInput(event) {
|
||||
this.newUsername = event.target.value;
|
||||
this.taken = false;
|
||||
this.errorMessage = null;
|
||||
|
||||
if (isEmpty(this.newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newUsername === this.args.user.username) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newUsername.length < this.minLength) {
|
||||
this.errorMessage = I18n.t("user.name.too_short");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await User.checkUsername(
|
||||
this.newUsername,
|
||||
undefined,
|
||||
this.args.user.id
|
||||
);
|
||||
|
||||
if (result.errors) {
|
||||
this.errorMessage = result.errors.join(" ");
|
||||
} else if (result.available === false) {
|
||||
this.taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changeUsername() {
|
||||
return this.dialog.yesNoConfirm({
|
||||
title: I18n.t("user.change_username.confirm"),
|
||||
didConfirm: async () => {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.args.user.changeUsername(this.newUsername);
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(this.newUsername.toLowerCase() + "/preferences")
|
||||
);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -64,13 +64,13 @@ export default Controller.extend({
|
||||
|
||||
@discourseComputed(
|
||||
"sidebarQueryParamOverride",
|
||||
"siteSettings.enable_sidebar",
|
||||
"siteSettings.navigation_menu",
|
||||
"canDisplaySidebar",
|
||||
"sidebarDisabledRouteOverride"
|
||||
)
|
||||
sidebarEnabled(
|
||||
sidebarQueryParamOverride,
|
||||
enableSidebar,
|
||||
navigationMenu,
|
||||
canDisplaySidebar,
|
||||
sidebarDisabledRouteOverride
|
||||
) {
|
||||
@ -95,7 +95,7 @@ export default Controller.extend({
|
||||
return false;
|
||||
}
|
||||
|
||||
return enableSidebar;
|
||||
return navigationMenu === "sidebar";
|
||||
},
|
||||
|
||||
calculateShowSidebar() {
|
||||
|
||||
@ -782,51 +782,62 @@ export default Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
groupsMentioned(groups) {
|
||||
groupsMentioned({ name, userCount, maxMentions }) {
|
||||
if (
|
||||
!this.get("model.creatingPrivateMessage") &&
|
||||
!this.get("model.topic.isPrivateMessage")
|
||||
this.get("model.creatingPrivateMessage") ||
|
||||
this.get("model.topic.isPrivateMessage")
|
||||
) {
|
||||
groups.forEach((group) => {
|
||||
let body;
|
||||
const groupLink = getURL(`/g/${group.name}/members`);
|
||||
const maxMentions = parseInt(group.max_mentions, 10);
|
||||
const userCount = parseInt(group.user_count, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxMentions < userCount) {
|
||||
body = I18n.t("composer.group_mentioned_limit", {
|
||||
group: `@${group.name}`,
|
||||
count: maxMentions,
|
||||
group_link: groupLink,
|
||||
});
|
||||
} else if (group.user_count > 0) {
|
||||
body = I18n.t("composer.group_mentioned", {
|
||||
group: `@${group.name}`,
|
||||
count: userCount,
|
||||
group_link: groupLink,
|
||||
});
|
||||
}
|
||||
maxMentions = parseInt(maxMentions, 10);
|
||||
userCount = parseInt(userCount, 10);
|
||||
|
||||
if (body) {
|
||||
this.appEvents.trigger("composer-messages:create", {
|
||||
extraClass: "custom-body",
|
||||
templateName: "education",
|
||||
body,
|
||||
});
|
||||
}
|
||||
let body;
|
||||
const groupLink = getURL(`/g/${name}/members`);
|
||||
|
||||
if (userCount > maxMentions) {
|
||||
body = I18n.t("composer.group_mentioned_limit", {
|
||||
group: `@${name}`,
|
||||
count: maxMentions,
|
||||
group_link: groupLink,
|
||||
});
|
||||
} else if (userCount > 0) {
|
||||
body = I18n.t("composer.group_mentioned", {
|
||||
group: `@${name}`,
|
||||
count: userCount,
|
||||
group_link: groupLink,
|
||||
});
|
||||
}
|
||||
|
||||
if (body) {
|
||||
this.appEvents.trigger("composer-messages:create", {
|
||||
extraClass: "custom-body",
|
||||
templateName: "education",
|
||||
body,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
cannotSeeMention(mentions) {
|
||||
mentions.forEach((mention) => {
|
||||
this.appEvents.trigger("composer-messages:create", {
|
||||
extraClass: "custom-body",
|
||||
templateName: "education",
|
||||
body: I18n.t(`composer.cannot_see_mention.${mention.reason}`, {
|
||||
username: mention.name,
|
||||
}),
|
||||
cannotSeeMention({ name, reason, notifiedCount, isGroup }) {
|
||||
notifiedCount = parseInt(notifiedCount, 10);
|
||||
|
||||
let body;
|
||||
if (isGroup) {
|
||||
body = I18n.t(`composer.cannot_see_group_mention.${reason}`, {
|
||||
group: name,
|
||||
count: notifiedCount,
|
||||
});
|
||||
} else {
|
||||
body = I18n.t(`composer.cannot_see_mention.${reason}`, {
|
||||
username: name,
|
||||
});
|
||||
}
|
||||
|
||||
this.appEvents.trigger("composer-messages:create", {
|
||||
extraClass: "custom-body",
|
||||
templateName: "education",
|
||||
body,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -192,7 +192,7 @@ export default Controller.extend(
|
||||
|
||||
@discourseComputed
|
||||
timeShortcuts() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
return [
|
||||
shortcuts.laterToday(),
|
||||
|
||||
@ -22,7 +22,7 @@ const controllerOpts = {
|
||||
|
||||
canStar: alias("currentUser.id"),
|
||||
showTopicPostBadges: not("new"),
|
||||
redirectedReason: alias("currentUser.redirected_to_top.reason"),
|
||||
redirectedReason: alias("currentUser.user_option.redirected_to_top.reason"),
|
||||
|
||||
expandGloballyPinned: false,
|
||||
expandAllPinned: false,
|
||||
|
||||
@ -10,10 +10,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||
@action
|
||||
downloadCalendar() {
|
||||
if (this.remember) {
|
||||
this.currentUser.setProperties({
|
||||
default_calendar: this.selectedCalendar,
|
||||
user_option: { default_calendar: this.selectedCalendar },
|
||||
});
|
||||
this.currentUser.user_option.set(
|
||||
"default_calendar",
|
||||
this.selectedCalendar
|
||||
);
|
||||
this.currentUser.save(["default_calendar"]);
|
||||
}
|
||||
if (this.selectedCalendar === "ics") {
|
||||
|
||||
@ -110,7 +110,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
|
||||
@discourseComputed
|
||||
timeShortcuts() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
|
||||
const nextWeek = shortcuts.monday();
|
||||
|
||||
@ -47,7 +47,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.currentUser.staff) {
|
||||
if (this.model.details.can_delete) {
|
||||
types.push({
|
||||
id: DELETE_STATUS_TYPE,
|
||||
name: I18n.t("topic.auto_delete.title"),
|
||||
@ -59,7 +59,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
name: I18n.t("topic.auto_bump.title"),
|
||||
});
|
||||
|
||||
if (this.currentUser.staff) {
|
||||
if (this.model.details.can_delete) {
|
||||
types.push({
|
||||
id: DELETE_REPLIES_TYPE,
|
||||
name: I18n.t("topic.auto_delete_replies.title"),
|
||||
|
||||
@ -13,7 +13,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
|
||||
@discourseComputed
|
||||
timeShortcuts() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
return [
|
||||
shortcuts.laterToday(),
|
||||
|
||||
@ -11,7 +11,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
|
||||
@discourseComputed
|
||||
timeShortcuts() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
return [
|
||||
shortcuts.laterToday(),
|
||||
|
||||
@ -225,30 +225,22 @@ export default Controller.extend(ModalFunctionality, {
|
||||
this.clearFlash();
|
||||
|
||||
if (
|
||||
(result.security_key_enabled ||
|
||||
result.totp_enabled ||
|
||||
result.backup_enabled) &&
|
||||
(result.security_key_enabled || result.totp_enabled) &&
|
||||
!this.secondFactorRequired
|
||||
) {
|
||||
let secondFactorMethod;
|
||||
if (result.security_key_enabled) {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.SECURITY_KEY;
|
||||
} else if (result.totp_enabled) {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.TOTP;
|
||||
} else {
|
||||
secondFactorMethod = SECOND_FACTOR_METHODS.BACKUP_CODE;
|
||||
}
|
||||
this.setProperties({
|
||||
otherMethodAllowed: result.multiple_second_factor_methods,
|
||||
secondFactorRequired: true,
|
||||
showLoginButtons: false,
|
||||
backupEnabled: result.backup_enabled,
|
||||
totpEnabled: result.totp_enabled,
|
||||
showSecondFactor: result.totp_enabled || result.backup_enabled,
|
||||
showSecondFactor: result.totp_enabled,
|
||||
showSecurityKey: result.security_key_enabled,
|
||||
secondFactorMethod: result.security_key_enabled
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP,
|
||||
securityKeyChallenge: result.challenge,
|
||||
securityKeyAllowedCredentialIds: result.allowed_credential_ids,
|
||||
secondFactorMethod,
|
||||
});
|
||||
|
||||
// only need to focus the 2FA input for TOTP
|
||||
|
||||
@ -82,13 +82,11 @@ export default Controller.extend({
|
||||
|
||||
@discourseComputed()
|
||||
emailFrequencyInstructions() {
|
||||
if (this.siteSettings.email_time_window_mins) {
|
||||
return I18n.t("user.email.frequency", {
|
||||
count: this.siteSettings.email_time_window_mins,
|
||||
});
|
||||
} else {
|
||||
return I18n.t("user.email.frequency_immediately");
|
||||
}
|
||||
return this.siteSettings.email_time_window_mins
|
||||
? I18n.t("user.email.frequency", {
|
||||
count: this.siteSettings.email_time_window_mins,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -51,7 +51,7 @@ export default Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("model.default_calendar")
|
||||
@discourseComputed("model.user_option.default_calendar")
|
||||
canChangeDefaultCalendar(defaultCalendar) {
|
||||
return defaultCalendar !== "none_selected";
|
||||
},
|
||||
@ -125,12 +125,6 @@ export default Controller.extend({
|
||||
return model
|
||||
.save(this.saveAttrNames)
|
||||
.then(() => {
|
||||
// update the timezone in memory so we can use the new
|
||||
// one if we change routes without reloading the user
|
||||
if (this.currentUser.id === this.model.id) {
|
||||
this.currentUser.timezone = this.model.user_option.timezone;
|
||||
}
|
||||
|
||||
cookAsync(model.get("bio_raw"))
|
||||
.then(() => {
|
||||
model.set("bio_cooked");
|
||||
|
||||
@ -10,6 +10,7 @@ import { findAll } from "discourse/models/login-method";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default Controller.extend(CanCheckEmails, {
|
||||
dialog: service(),
|
||||
@ -113,6 +114,29 @@ export default Controller.extend(CanCheckEmails, {
|
||||
.finally(() => this.set("resetPasswordLoading", false));
|
||||
},
|
||||
|
||||
disableAllMessage() {
|
||||
let templateElements = [I18n.t("user.second_factor.delete_confirm_header")];
|
||||
templateElements.push("<ul>");
|
||||
this.totps.forEach((totp) => {
|
||||
templateElements.push(`<li>${totp.name}</li>`);
|
||||
});
|
||||
this.security_keys.forEach((key) => {
|
||||
templateElements.push(`<li>${key.name}</li>`);
|
||||
});
|
||||
if (this.currentUser.second_factor_backup_enabled) {
|
||||
templateElements.push(
|
||||
`<li>${I18n.t("user.second_factor_backup.title")}</li>`
|
||||
);
|
||||
}
|
||||
templateElements.push("</ul>");
|
||||
templateElements.push(
|
||||
I18n.t("user.second_factor.delete_confirm_instruction", {
|
||||
confirm: I18n.t("user.second_factor.disable"),
|
||||
})
|
||||
);
|
||||
return htmlSafe(templateElements.join(""));
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirmPassword() {
|
||||
if (!this.password) {
|
||||
@ -130,8 +154,11 @@ export default Controller.extend(CanCheckEmails, {
|
||||
|
||||
this.dialog.deleteConfirm({
|
||||
title: I18n.t("user.second_factor.disable_confirm"),
|
||||
message: this.disableAllMessage(),
|
||||
confirmButtonLabel: "user.second_factor.disable",
|
||||
confirmPhrase: I18n.t("user.second_factor.disable"),
|
||||
confirmButtonIcon: "ban",
|
||||
cancelButtonClass: "btn-flat",
|
||||
didConfirm: () => {
|
||||
this.model
|
||||
.disableAllSecondFactors()
|
||||
@ -144,6 +171,88 @@ export default Controller.extend(CanCheckEmails, {
|
||||
},
|
||||
});
|
||||
},
|
||||
disableSingleSecondFactor(secondFactorMethod) {
|
||||
if (this.totps.concat(this.security_keys).length === 1) {
|
||||
this.send("disableAllSecondFactors");
|
||||
return;
|
||||
}
|
||||
this.dialog.deleteConfirm({
|
||||
title: I18n.t("user.second_factor.delete_single_confirm_title"),
|
||||
message: I18n.t("user.second_factor.delete_single_confirm_message", {
|
||||
name: secondFactorMethod.name,
|
||||
}),
|
||||
confirmButtonLabel: "user.second_factor.delete",
|
||||
confirmButtonIcon: "ban",
|
||||
cancelButtonClass: "btn-flat",
|
||||
didConfirm: () => {
|
||||
this.currentUser
|
||||
.updateSecondFactor(
|
||||
secondFactorMethod.id,
|
||||
secondFactorMethod.name,
|
||||
true,
|
||||
secondFactorMethod.method
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch((e) => this.handleError(e))
|
||||
.finally(() => {
|
||||
this.setProperties({
|
||||
totps: this.totps.filter(
|
||||
(totp) =>
|
||||
totp.id !== secondFactorMethod.id ||
|
||||
totp.method !== secondFactorMethod.method
|
||||
),
|
||||
security_keys: this.security_keys.filter(
|
||||
(key) =>
|
||||
key.id !== secondFactorMethod.id ||
|
||||
key.method !== secondFactorMethod.method
|
||||
),
|
||||
});
|
||||
|
||||
this.set("loading", false);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
disableSecondFactorBackup() {
|
||||
this.dialog.deleteConfirm({
|
||||
title: I18n.t("user.second_factor.delete_backup_codes_confirm_title"),
|
||||
message: I18n.t(
|
||||
"user.second_factor.delete_backup_codes_confirm_message"
|
||||
),
|
||||
confirmButtonLabel: "user.second_factor.delete",
|
||||
confirmButtonIcon: "ban",
|
||||
cancelButtonClass: "btn-flat",
|
||||
didConfirm: () => {
|
||||
this.set("backupCodes", []);
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("errorMessage", null);
|
||||
this.model.set("second_factor_backup_enabled", false);
|
||||
this.markDirty();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((error) => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
createTotp() {
|
||||
const controller = showModal("second-factor-add-totp", {
|
||||
|
||||
@ -13,6 +13,12 @@ export default class extends Controller {
|
||||
@tracked selectedSidebarCategories = [];
|
||||
@tracked selectedSidebarTagNames = [];
|
||||
|
||||
saveAttrNames = [
|
||||
"sidebar_category_ids",
|
||||
"sidebar_tag_names",
|
||||
"sidebar_list_destination",
|
||||
];
|
||||
|
||||
sidebarListDestinations = [
|
||||
{
|
||||
name: I18n.t("user.experimental_sidebar.list_destination_default"),
|
||||
@ -42,7 +48,7 @@ export default class extends Controller {
|
||||
);
|
||||
|
||||
this.model
|
||||
.save()
|
||||
.save(this.saveAttrNames)
|
||||
.then((result) => {
|
||||
if (result.user.sidebar_tags) {
|
||||
this.model.set("sidebar_tags", result.user.sidebar_tags);
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import { empty, or } from "@ember/object/computed";
|
||||
import { propertyEqual, setting } from "discourse/lib/computed";
|
||||
import Controller from "@ember/controller";
|
||||
import I18n from "I18n";
|
||||
import User from "discourse/models/user";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Controller.extend({
|
||||
dialog: service(),
|
||||
taken: false,
|
||||
saving: false,
|
||||
errorMessage: null,
|
||||
newUsername: null,
|
||||
|
||||
maxLength: setting("max_username_length"),
|
||||
minLength: setting("min_username_length"),
|
||||
newUsernameEmpty: empty("newUsername"),
|
||||
saveDisabled: or(
|
||||
"saving",
|
||||
"newUsernameEmpty",
|
||||
"taken",
|
||||
"unchanged",
|
||||
"errorMessage"
|
||||
),
|
||||
unchanged: propertyEqual("newUsername", "username"),
|
||||
|
||||
@observes("newUsername")
|
||||
checkTaken() {
|
||||
let newUsername = this.newUsername;
|
||||
|
||||
if (newUsername && newUsername.length < this.minLength) {
|
||||
this.set("errorMessage", I18n.t("user.name.too_short"));
|
||||
} else {
|
||||
this.set("taken", false);
|
||||
this.set("errorMessage", null);
|
||||
|
||||
if (isEmpty(this.newUsername)) {
|
||||
return;
|
||||
}
|
||||
if (this.unchanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
User.checkUsername(newUsername, undefined, this.get("model.id")).then(
|
||||
(result) => {
|
||||
if (result.errors) {
|
||||
this.set("errorMessage", result.errors.join(" "));
|
||||
} else if (result.available === false) {
|
||||
this.set("taken", true);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("saving")
|
||||
saveButtonText(saving) {
|
||||
if (saving) {
|
||||
return I18n.t("saving");
|
||||
}
|
||||
return I18n.t("user.change");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeUsername() {
|
||||
if (this.saveDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.dialog.yesNoConfirm({
|
||||
title: I18n.t("user.change_username.confirm"),
|
||||
didConfirm: () => {
|
||||
this.set("saving", true);
|
||||
this.model
|
||||
.changeUsername(this.newUsername)
|
||||
.then(() => {
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(this.newUsername.toLowerCase() + "/preferences")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("saving", false));
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -40,30 +40,6 @@ export default Controller.extend(ModalFunctionality, {
|
||||
this._hideCopyMessage();
|
||||
},
|
||||
|
||||
disableSecondFactorBackup() {
|
||||
this.set("backupCodes", []);
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("errorMessage", null);
|
||||
this.model.set("second_factor_backup_enabled", false);
|
||||
this.markDirty();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch((error) => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
generateSecondFactorCodes() {
|
||||
this.set("loading", true);
|
||||
this.model
|
||||
|
||||
@ -3,30 +3,6 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
actions: {
|
||||
disableSecondFactor() {
|
||||
this.user
|
||||
.updateSecondFactor(
|
||||
this.model.id,
|
||||
this.model.name,
|
||||
true,
|
||||
this.model.method
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.send("closeModal");
|
||||
});
|
||||
},
|
||||
|
||||
editSecondFactor() {
|
||||
this.user
|
||||
.updateSecondFactor(
|
||||
|
||||
@ -62,7 +62,7 @@ addBulkButton("deletePostTiming", "defer", {
|
||||
icon: "circle",
|
||||
class: "btn-default",
|
||||
buttonVisible() {
|
||||
return this.currentUser.enable_defer;
|
||||
return this.currentUser.user_option.enable_defer;
|
||||
},
|
||||
});
|
||||
addBulkButton("unlistTopics", "unlist_topics", {
|
||||
|
||||
@ -2,7 +2,10 @@ import Category from "discourse/models/category";
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import { alias, and, not, or } from "@ember/object/computed";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import discourseComputed, {
|
||||
bind,
|
||||
observes,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import { isEmpty, isPresent } from "@ember/utils";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
@ -837,7 +840,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
bookmarkable_id: post.id,
|
||||
bookmarkable_type: "Post",
|
||||
auto_delete_preference:
|
||||
this.currentUser.bookmark_auto_delete_preference,
|
||||
this.currentUser.user_option.bookmark_auto_delete_preference,
|
||||
}),
|
||||
post
|
||||
);
|
||||
@ -1350,7 +1353,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
bookmarkable_id: this.model.id,
|
||||
bookmarkable_type: "Topic",
|
||||
auto_delete_preference:
|
||||
this.currentUser.bookmark_auto_delete_preference,
|
||||
this.currentUser.user_option.bookmark_auto_delete_preference,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -1599,157 +1602,9 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
subscribe() {
|
||||
this.unsubscribe();
|
||||
|
||||
const refresh = (args) =>
|
||||
this.appEvents.trigger("post-stream:refresh", args);
|
||||
|
||||
this.messageBus.subscribe(
|
||||
`/topic/${this.get("model.id")}`,
|
||||
(data) => {
|
||||
const topic = this.model;
|
||||
|
||||
if (isPresent(data.notification_level_change)) {
|
||||
topic.set(
|
||||
"details.notification_level",
|
||||
data.notification_level_change
|
||||
);
|
||||
topic.set(
|
||||
"details.notifications_reason_id",
|
||||
data.notifications_reason_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const postStream = this.get("model.postStream");
|
||||
|
||||
if (data.reload_topic) {
|
||||
topic.reload().then(() => {
|
||||
this.send("postChangedRoute", topic.get("post_number") || 1);
|
||||
this.appEvents.trigger("header:update-topic", topic);
|
||||
if (data.refresh_stream) {
|
||||
postStream.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "acted":
|
||||
postStream
|
||||
.triggerChangedPost(data.id, data.updated_at, {
|
||||
preserveCooked: true,
|
||||
})
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
case "read": {
|
||||
postStream
|
||||
.triggerReadPost(data.id, data.readers_count)
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
}
|
||||
case "liked":
|
||||
case "unliked": {
|
||||
postStream
|
||||
.triggerLikedPost(
|
||||
data.id,
|
||||
data.likes_count,
|
||||
data.user_id,
|
||||
data.type
|
||||
)
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
}
|
||||
case "revised":
|
||||
case "rebaked": {
|
||||
postStream
|
||||
.triggerChangedPost(data.id, data.updated_at)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "deleted": {
|
||||
postStream
|
||||
.triggerDeletedPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "destroyed": {
|
||||
postStream
|
||||
.triggerDestroyedPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "recovered": {
|
||||
postStream
|
||||
.triggerRecoveredPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "created": {
|
||||
this._newPostsInStream.push(data.id);
|
||||
|
||||
this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => {
|
||||
const postIds = this._newPostsInStream;
|
||||
this._newPostsInStream = [];
|
||||
|
||||
return postStream
|
||||
.triggerNewPostsInStream(postIds, { background: true })
|
||||
.then(() => refresh())
|
||||
.catch((e) => {
|
||||
this._newPostsInStream = postIds.concat(
|
||||
this._newPostsInStream
|
||||
);
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
if (this.get("currentUser.id") !== data.user_id) {
|
||||
this.documentTitle.incrementBackgroundContextCount();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "move_to_inbox": {
|
||||
topic.set("message_archived", false);
|
||||
break;
|
||||
}
|
||||
case "archived": {
|
||||
topic.set("message_archived", true);
|
||||
break;
|
||||
}
|
||||
case "stats": {
|
||||
let updateStream = false;
|
||||
["last_posted_at", "like_count", "posts_count"].forEach(
|
||||
(property) => {
|
||||
const value = data[property];
|
||||
if (typeof value !== "undefined") {
|
||||
topic.set(property, value);
|
||||
updateStream = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (data["last_poster"]) {
|
||||
topic.details.set("last_poster", data["last_poster"]);
|
||||
updateStream = true;
|
||||
}
|
||||
|
||||
if (updateStream) {
|
||||
postStream
|
||||
.triggerChangedTopicStats()
|
||||
.then((firstPostId) => refresh({ id: firstPostId }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let callback = customPostMessageCallbacks[data.type];
|
||||
if (callback) {
|
||||
callback(this, data);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("unknown topic bus message type", data);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
this.onMessage,
|
||||
this.get("model.message_bus_last_id")
|
||||
);
|
||||
},
|
||||
@ -1759,7 +1614,146 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
if (!this.get("model.id")) {
|
||||
return;
|
||||
}
|
||||
this.messageBus.unsubscribe("/topic/*");
|
||||
|
||||
this.messageBus.unsubscribe("/topic/*", this.onMessage);
|
||||
},
|
||||
|
||||
@bind
|
||||
onMessage(data) {
|
||||
const topic = this.model;
|
||||
const refresh = (args) =>
|
||||
this.appEvents.trigger("post-stream:refresh", args);
|
||||
|
||||
if (isPresent(data.notification_level_change)) {
|
||||
topic.set("details.notification_level", data.notification_level_change);
|
||||
topic.set(
|
||||
"details.notifications_reason_id",
|
||||
data.notifications_reason_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const postStream = this.get("model.postStream");
|
||||
|
||||
if (data.reload_topic) {
|
||||
topic.reload().then(() => {
|
||||
this.send("postChangedRoute", topic.get("post_number") || 1);
|
||||
this.appEvents.trigger("header:update-topic", topic);
|
||||
if (data.refresh_stream) {
|
||||
postStream.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "acted":
|
||||
postStream
|
||||
.triggerChangedPost(data.id, data.updated_at, {
|
||||
preserveCooked: true,
|
||||
})
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
case "read": {
|
||||
postStream
|
||||
.triggerReadPost(data.id, data.readers_count)
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
}
|
||||
case "liked":
|
||||
case "unliked": {
|
||||
postStream
|
||||
.triggerLikedPost(data.id, data.likes_count, data.user_id, data.type)
|
||||
.then(() => refresh({ id: data.id, refreshLikes: true }));
|
||||
break;
|
||||
}
|
||||
case "revised":
|
||||
case "rebaked": {
|
||||
postStream
|
||||
.triggerChangedPost(data.id, data.updated_at)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "deleted": {
|
||||
postStream
|
||||
.triggerDeletedPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "destroyed": {
|
||||
postStream
|
||||
.triggerDestroyedPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "recovered": {
|
||||
postStream
|
||||
.triggerRecoveredPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "created": {
|
||||
this._newPostsInStream.push(data.id);
|
||||
|
||||
this.retryOnRateLimit(RETRIES_ON_RATE_LIMIT, () => {
|
||||
const postIds = this._newPostsInStream;
|
||||
this._newPostsInStream = [];
|
||||
|
||||
return postStream
|
||||
.triggerNewPostsInStream(postIds, { background: true })
|
||||
.then(() => refresh())
|
||||
.catch((e) => {
|
||||
this._newPostsInStream = postIds.concat(this._newPostsInStream);
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
if (this.get("currentUser.id") !== data.user_id) {
|
||||
this.documentTitle.incrementBackgroundContextCount();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "move_to_inbox": {
|
||||
topic.set("message_archived", false);
|
||||
break;
|
||||
}
|
||||
case "archived": {
|
||||
topic.set("message_archived", true);
|
||||
break;
|
||||
}
|
||||
case "stats": {
|
||||
let updateStream = false;
|
||||
["last_posted_at", "like_count", "posts_count"].forEach((property) => {
|
||||
const value = data[property];
|
||||
if (typeof value !== "undefined") {
|
||||
topic.set(property, value);
|
||||
updateStream = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (data["last_poster"]) {
|
||||
topic.details.set("last_poster", data["last_poster"]);
|
||||
updateStream = true;
|
||||
}
|
||||
|
||||
if (updateStream) {
|
||||
postStream
|
||||
.triggerChangedTopicStats()
|
||||
.then((firstPostId) => refresh({ id: firstPostId }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let callback = customPostMessageCallbacks[data.type];
|
||||
if (callback) {
|
||||
callback(this, data);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("unknown topic bus message type", data);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reply() {
|
||||
@ -1781,7 +1775,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
if (
|
||||
this.siteSettings.automatically_unpin_topics &&
|
||||
this.currentUser &&
|
||||
this.currentUser.automatically_unpin_topics
|
||||
this.currentUser.user_option.automatically_unpin_topics
|
||||
) {
|
||||
// automatically unpin topics when the user reaches the bottom
|
||||
const max = Math.max(...postNumbers);
|
||||
|
||||
@ -2,11 +2,27 @@ import Controller, { inject as controller } from "@ember/controller";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { alias, and, equal, readOnly } from "@ember/object/computed";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import I18n from "I18n";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
|
||||
export const PERSONAL_INBOX = "__personal_inbox__";
|
||||
const customUserNavMessagesDropdownRows = [];
|
||||
|
||||
export function registerCustomUserNavMessagesDropdownRow(
|
||||
routeName,
|
||||
name,
|
||||
icon
|
||||
) {
|
||||
customUserNavMessagesDropdownRows.push({
|
||||
routeName,
|
||||
name,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetCustomUserNavMessagesDropdownRows() {
|
||||
customUserNavMessagesDropdownRows.length = 0;
|
||||
}
|
||||
|
||||
export default class extends Controller {
|
||||
@service router;
|
||||
@ -25,19 +41,21 @@ export default class extends Controller {
|
||||
@readOnly("site.can_tag_pms") pmTaggingEnabled;
|
||||
|
||||
get messagesDropdownValue() {
|
||||
const parentRoute = this.router.currentRoute.parent;
|
||||
let value;
|
||||
|
||||
if (Object.keys(parentRoute.params).length > 0) {
|
||||
return this.router.urlFor(
|
||||
parentRoute.name,
|
||||
this.model.username,
|
||||
parentRoute.params
|
||||
);
|
||||
} else {
|
||||
return this.router.urlFor(parentRoute.name, this.model.username);
|
||||
for (let i = this.messagesDropdownContent.length - 1; i >= 0; i--) {
|
||||
const row = this.messagesDropdownContent[i];
|
||||
|
||||
if (this.router.currentURL.includes(row.id)) {
|
||||
value = row.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@cached
|
||||
get messagesDropdownContent() {
|
||||
const content = [
|
||||
{
|
||||
@ -66,6 +84,14 @@ export default class extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
customUserNavMessagesDropdownRows.forEach((row) => {
|
||||
content.push({
|
||||
id: this.router.urlFor(row.routeName, this.model.username),
|
||||
name: row.name,
|
||||
icon: row.icon,
|
||||
});
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
},
|
||||
|
||||
_buildTimeShortcuts() {
|
||||
const timezone = this.currentUser.timezone;
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
return [shortcuts.oneHour(), shortcuts.twoHours(), shortcuts.tomorrow()];
|
||||
},
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { helperContext, registerUnbound } from "discourse-common/lib/helpers";
|
||||
import Category from "discourse/models/category";
|
||||
import I18n from "I18n";
|
||||
import Site from "discourse/models/site";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { get } from "@ember/object";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
@ -39,13 +38,13 @@ export function addExtraIconRenderer(renderer) {
|
||||
@param {Number} [opts.depth] Current category depth, used for limiting recursive calls
|
||||
**/
|
||||
export function categoryBadgeHTML(category, opts) {
|
||||
let siteSettings = helperContext().siteSettings;
|
||||
const { site, siteSettings } = helperContext();
|
||||
opts = opts || {};
|
||||
|
||||
if (
|
||||
!category ||
|
||||
(!opts.allowUncategorized &&
|
||||
get(category, "id") === Site.currentProp("uncategorized_category_id") &&
|
||||
get(category, "id") === site.uncategorized_category_id &&
|
||||
siteSettings.suppress_uncategorized_badge)
|
||||
) {
|
||||
return "";
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import EmberObject from "@ember/object";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
|
||||
export default {
|
||||
@ -6,14 +7,21 @@ export default {
|
||||
after: "message-bus",
|
||||
|
||||
initialize(container) {
|
||||
const site = container.lookup("service:site");
|
||||
this.site = container.lookup("service:site");
|
||||
this.messageBus = container.lookup("service:message-bus");
|
||||
|
||||
const banner = EmberObject.create(PreloadStore.get("banner") || {});
|
||||
const messageBus = container.lookup("service:message-bus");
|
||||
this.site.set("banner", banner);
|
||||
|
||||
site.set("banner", banner);
|
||||
this.messageBus.subscribe("/site/banner", this.onMessage);
|
||||
},
|
||||
|
||||
messageBus.subscribe("/site/banner", (data) => {
|
||||
site.set("banner", EmberObject.create(data || {}));
|
||||
});
|
||||
teardown() {
|
||||
this.messageBus.unsubscribe("/site/banner", this.onMessage);
|
||||
},
|
||||
|
||||
@bind
|
||||
onMessage(data = {}) {
|
||||
this.site.set("banner", EmberObject.create(data));
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { isDevelopment } from "discourse-common/config/environment";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
// Use the message bus for live reloading of components for faster development.
|
||||
export default {
|
||||
name: "live-development",
|
||||
|
||||
initialize(container) {
|
||||
const messageBus = container.lookup("service:message-bus");
|
||||
this.messageBus = container.lookup("service:message-bus");
|
||||
const session = container.lookup("service:session");
|
||||
|
||||
// Preserve preview_theme_id=## and pp=async-flamegraph parameters across pages
|
||||
@ -38,37 +39,46 @@ export default {
|
||||
}
|
||||
|
||||
// Observe file changes
|
||||
messageBus.subscribe(
|
||||
this.messageBus.subscribe(
|
||||
"/file-change",
|
||||
(data) => {
|
||||
data.forEach((me) => {
|
||||
if (me === "refresh") {
|
||||
// Refresh if necessary
|
||||
document.location.reload(true);
|
||||
} else if (me.new_href && me.target) {
|
||||
const link_target = !!me.theme_id
|
||||
? `[data-target='${me.target}'][data-theme-id='${me.theme_id}']`
|
||||
: `[data-target='${me.target}']`;
|
||||
|
||||
const links = document.querySelectorAll(`link${link_target}`);
|
||||
if (links.length > 0) {
|
||||
const lastLink = links[links.length - 1];
|
||||
// this check is useful when message-bus has multiple file updates
|
||||
// it avoids the browser doing a lot of work for nothing
|
||||
// should the filenames be unchanged
|
||||
if (
|
||||
lastLink.href.split("/").pop() !== me.new_href.split("/").pop()
|
||||
) {
|
||||
this.refreshCSS(lastLink, me.new_href);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
this.onFileChange,
|
||||
session.mbLastFileChangeId
|
||||
);
|
||||
},
|
||||
|
||||
teardown() {
|
||||
this.messageBus.unsubscribe("/file-change", this.onFileChange);
|
||||
},
|
||||
|
||||
@bind
|
||||
onFileChange(data) {
|
||||
data.forEach((me) => {
|
||||
if (me === "refresh") {
|
||||
// Refresh if necessary
|
||||
document.location.reload(true);
|
||||
} else if (me.new_href && me.target) {
|
||||
let query = `link[data-target='${me.target}']`;
|
||||
|
||||
if (me.theme_id) {
|
||||
query += `[data-theme-id='${me.theme_id}']`;
|
||||
}
|
||||
|
||||
const links = document.querySelectorAll(query);
|
||||
|
||||
if (links.length > 0) {
|
||||
const lastLink = links[links.length - 1];
|
||||
|
||||
// this check is useful when message-bus has multiple file updates
|
||||
// it avoids the browser doing a lot of work for nothing
|
||||
// should the filenames be unchanged
|
||||
if (lastLink.href.split("/").pop() !== me.new_href.split("/").pop()) {
|
||||
this.refreshCSS(lastLink, me.new_href);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refreshCSS(node, newHref) {
|
||||
const reloaded = node.cloneNode(true);
|
||||
reloaded.href = newHref;
|
||||
|
||||
@ -1,35 +1,39 @@
|
||||
import I18n from "I18n";
|
||||
import logout from "discourse/lib/logout";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
let _showingLogout = false;
|
||||
|
||||
// Subscribe to "logout" change events via the Message Bus
|
||||
// Subscribe to "logout" change events via the Message Bus
|
||||
export default {
|
||||
name: "logout",
|
||||
after: "message-bus",
|
||||
|
||||
initialize(container) {
|
||||
const messageBus = container.lookup("service:message-bus"),
|
||||
dialog = container.lookup("service:dialog");
|
||||
this.messageBus = container.lookup("service:message-bus");
|
||||
this.dialog = container.lookup("service:dialog");
|
||||
|
||||
if (!messageBus) {
|
||||
this.messageBus.subscribe("/logout", this.onMessage);
|
||||
},
|
||||
|
||||
teardown() {
|
||||
this.messageBus.unsubscribe("/logout", this.onMessage);
|
||||
},
|
||||
|
||||
@bind
|
||||
onMessage() {
|
||||
if (_showingLogout) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageBus.subscribe("/logout", function () {
|
||||
if (!_showingLogout) {
|
||||
_showingLogout = true;
|
||||
_showingLogout = true;
|
||||
|
||||
dialog.alert({
|
||||
message: I18n.t("logout"),
|
||||
confirmButtonLabel: "home",
|
||||
didConfirm: logout,
|
||||
didCancel: logout,
|
||||
shouldDisplayCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
_showingLogout = true;
|
||||
this.dialog.alert({
|
||||
message: I18n.t("logout"),
|
||||
confirmButtonLabel: "home",
|
||||
didConfirm: logout,
|
||||
didCancel: logout,
|
||||
shouldDisplayCancel: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user