Version bump
38
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: bundler
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "08:00"
|
||||
timezone: Australia/Sydney
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: lockfile-only
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
ignore:
|
||||
- dependency-name: aws-partitions
|
||||
versions:
|
||||
- "> 1.329.0"
|
||||
- "< 2"
|
||||
- dependency-name: aws-sdk-core
|
||||
versions:
|
||||
- "> 3.99.1"
|
||||
- "< 4"
|
||||
- dependency-name: aws-sdk-kms
|
||||
versions:
|
||||
- "> 1.31.0"
|
||||
- "< 2"
|
||||
- dependency-name: aws-sdk-s3
|
||||
versions:
|
||||
- "> 1.66.0"
|
||||
- "< 2"
|
||||
- dependency-name: aws-sdk-sns
|
||||
versions:
|
||||
- "> 1.25.1"
|
||||
- "< 2"
|
||||
- dependency-name: aws-sigv4
|
||||
versions:
|
||||
- "> 1.2.0"
|
||||
- "< 2"
|
||||
6
.gitignore
vendored
@ -137,6 +137,12 @@ node_modules
|
||||
# ignore generated api documentation files
|
||||
openapi/*
|
||||
|
||||
# ignore VSCode config files
|
||||
.vscode
|
||||
|
||||
# ignore direnv
|
||||
.envrc
|
||||
|
||||
# ember-cli generated
|
||||
dist
|
||||
|
||||
|
||||
77
Gemfile.lock
@ -45,34 +45,34 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.329.0)
|
||||
aws-sdk-core (3.99.1)
|
||||
aws-partitions (1.390.0)
|
||||
aws-sdk-core (3.109.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sdk-kms (1.39.0)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.66.0)
|
||||
aws-sdk-core (~> 3, >= 3.96.1)
|
||||
aws-sdk-s3 (1.83.2)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-sns (1.25.1)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sdk-sns (1.35.0)
|
||||
aws-sdk-core (~> 3, >= 3.109.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.0)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
barber (0.12.2)
|
||||
ember-source (>= 1.0, < 3.1)
|
||||
execjs (>= 1.2, < 3)
|
||||
better_errors (2.8.3)
|
||||
better_errors (2.9.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.4.9)
|
||||
bootsnap (1.5.1)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
@ -120,7 +120,7 @@ GEM
|
||||
barber (>= 0.11.0)
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.9.0)
|
||||
erubi (1.10.0)
|
||||
excon (0.78.0)
|
||||
execjs (2.7.0)
|
||||
exifr (1.3.9)
|
||||
@ -160,7 +160,7 @@ GEM
|
||||
jwt (2.2.2)
|
||||
kgio (2.11.3)
|
||||
libv8 (8.4.255.0)
|
||||
listen (3.2.1)
|
||||
listen (3.3.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
lograge (0.11.2)
|
||||
@ -213,7 +213,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.10.15)
|
||||
oj (3.10.16)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -235,7 +235,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (2.1.4)
|
||||
onebox (2.1.6)
|
||||
addressable (~> 2.7.0)
|
||||
htmlentities (~> 4.3)
|
||||
multi_json (~> 1.11)
|
||||
@ -244,7 +244,7 @@ GEM
|
||||
sanitize
|
||||
openssl-signature_algorithm (1.0.0)
|
||||
optimist (3.0.1)
|
||||
parallel (1.19.2)
|
||||
parallel (1.20.0)
|
||||
parallel_tests (3.3.0)
|
||||
parallel
|
||||
parser (2.7.2.0)
|
||||
@ -275,7 +275,7 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails_failover (0.5.7)
|
||||
rails_failover (0.6.0)
|
||||
activerecord (~> 6.0)
|
||||
railties (~> 6.0)
|
||||
rails_multisite (2.5.0)
|
||||
@ -311,21 +311,21 @@ GEM
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec (3.9.0)
|
||||
rspec-core (~> 3.9.0)
|
||||
rspec-expectations (~> 3.9.0)
|
||||
rspec-mocks (~> 3.9.0)
|
||||
rspec-core (3.9.3)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.4)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-html-matchers (0.9.4)
|
||||
nokogiri (~> 1)
|
||||
rspec (>= 3.0.0.a, < 4)
|
||||
rspec-mocks (3.9.1)
|
||||
rspec-mocks (3.10.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
@ -334,29 +334,30 @@ GEM
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.4)
|
||||
rspec-support (3.10.0)
|
||||
rswag-specs (2.3.1)
|
||||
activesupport (>= 3.1, < 7.0)
|
||||
json-schema (~> 2.2)
|
||||
railties (>= 3.1, < 7.0)
|
||||
rtlit (0.0.5)
|
||||
rubocop (1.1.0)
|
||||
rubocop (1.3.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.5)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8)
|
||||
rexml
|
||||
rubocop-ast (>= 1.0.1)
|
||||
rubocop-ast (>= 1.1.1)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (1.1.0)
|
||||
rubocop-ast (1.1.1)
|
||||
parser (>= 2.7.1.5)
|
||||
rubocop-discourse (2.4.0)
|
||||
rubocop-discourse (2.4.1)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0.pre)
|
||||
rubocop-rspec (2.0.0.pre)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.0.0)
|
||||
rubocop (~> 1.0)
|
||||
ruby-prof (1.4.1)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
ruby-prof (1.4.2)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
@ -402,7 +403,7 @@ GEM
|
||||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
tzinfo (1.2.7)
|
||||
tzinfo (1.2.8)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
@ -414,11 +415,11 @@ GEM
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.13.0)
|
||||
webmock (3.9.3)
|
||||
webmock (3.10.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpush (1.0.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
xorcist (1.1.2)
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 937 B |
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 898 B |
|
Before Width: | Height: | Size: 895 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 613 B After Width: | Height: | Size: 525 B |
|
Before Width: | Height: | Size: 845 B After Width: | Height: | Size: 759 B |
@ -2,6 +2,7 @@ import Component from "@ember/component";
|
||||
import BufferedContent from "discourse/mixins/buffered-content";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
import SettingComponent from "admin/mixins/setting-component";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
|
||||
export default Component.extend(BufferedContent, SettingComponent, {
|
||||
updateExistingUsers: null,
|
||||
@ -12,4 +13,6 @@ export default Component.extend(BufferedContent, SettingComponent, {
|
||||
updateExistingUsers: this.updateExistingUsers,
|
||||
});
|
||||
},
|
||||
|
||||
staffLogFilter: readOnly("setting.staffLogFilter"),
|
||||
});
|
||||
|
||||
@ -1,4 +1,52 @@
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "I18n";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
const CUSTOM_REASON_KEY = "custom";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
selectedReason: CUSTOM_REASON_KEY,
|
||||
customReason: "",
|
||||
reasonKeys: [
|
||||
"not_listening_to_staff",
|
||||
"consuming_staff_time",
|
||||
"combatative",
|
||||
"in_wrong_place",
|
||||
"no_constructive_purpose",
|
||||
CUSTOM_REASON_KEY,
|
||||
],
|
||||
isCustomReason: equal("selectedReason", CUSTOM_REASON_KEY),
|
||||
|
||||
@discourseComputed("reasonKeys")
|
||||
reasons(keys) {
|
||||
return keys.map((key) => {
|
||||
return { id: key, name: I18n.t(`admin.user.suspend_reasons.${key}`) };
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
setSelectedReason(value) {
|
||||
this.set("selectedReason", value);
|
||||
this.setReason();
|
||||
},
|
||||
|
||||
@action
|
||||
setCustomReason(value) {
|
||||
this.set("customReason", value);
|
||||
this.setReason();
|
||||
},
|
||||
|
||||
setReason() {
|
||||
if (this.isCustomReason) {
|
||||
this.set("reason", this.customReason);
|
||||
} else {
|
||||
this.set(
|
||||
"reason",
|
||||
I18n.t(`admin.user.suspend_reasons.${this.selectedReason}`)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -35,6 +35,11 @@ export default Controller.extend({
|
||||
childThemesNames: mapBy("model.childThemes", "name"),
|
||||
extraFiles: filterBy("model.theme_fields", "target", "extra_js"),
|
||||
|
||||
@discourseComputed("model.component", "model.remote_theme")
|
||||
showCheckboxes() {
|
||||
return !this.model.component || this.model.remote_theme;
|
||||
},
|
||||
|
||||
@discourseComputed("model.editedFields")
|
||||
editedFieldsFormatted() {
|
||||
const descriptions = [];
|
||||
@ -304,6 +309,10 @@ export default Controller.extend({
|
||||
this.model.saveChanges("user_selectable");
|
||||
},
|
||||
|
||||
applyAutoUpdateable() {
|
||||
this.model.saveChanges("auto_update");
|
||||
},
|
||||
|
||||
addChildTheme() {
|
||||
let themeId = parseInt(this.selectedChildThemeId, 10);
|
||||
let theme = this.allThemes.findBy("id", themeId);
|
||||
|
||||
@ -4,7 +4,7 @@ import Controller from "@ember/controller";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
const MAX_FIELDS = 20;
|
||||
const MAX_FIELDS = 30;
|
||||
|
||||
export default Controller.extend({
|
||||
fieldTypes: null,
|
||||
|
||||
@ -19,6 +19,7 @@ export default Controller.extend(CanCheckEmails, {
|
||||
customGroupIdsBuffer: null,
|
||||
availableGroups: null,
|
||||
userTitleValue: null,
|
||||
ssoExternalEmail: null,
|
||||
|
||||
showBadges: setting("enable_badges"),
|
||||
hasLockedTrustLevel: notEmpty("model.manual_locked_trust_level"),
|
||||
@ -339,5 +340,15 @@ export default Controller.extend(CanCheckEmails, {
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
checkSsoEmail() {
|
||||
return ajax(userPath(`${this.model.username_lower}/sso-email.json`), {
|
||||
data: { context: window.location.pathname },
|
||||
}).then((result) => {
|
||||
if (result) {
|
||||
this.set("ssoExternalEmail", result.email);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import I18n from "I18n";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import { next } from "@ember/runloop";
|
||||
import { Promise } from "rsvp";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Mixin.create(ModalFunctionality, {
|
||||
errorMessage: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
postEdit: null,
|
||||
@ -18,6 +19,7 @@ export default Mixin.create(ModalFunctionality, {
|
||||
|
||||
resetModal() {
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
loadingUser: true,
|
||||
@ -66,6 +68,8 @@ export default Mixin.create(ModalFunctionality, {
|
||||
callback(result);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
.catch((error) => {
|
||||
this.set("errorMessage", extractError(error));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -352,28 +352,17 @@ const AdminUser = User.extend({
|
||||
type: "PUT",
|
||||
})
|
||||
.then((result) => this.setProperties(result.unsilence))
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.unsilence_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
})
|
||||
.finally(() => this.set("silencingUser", false));
|
||||
},
|
||||
|
||||
silence(data) {
|
||||
this.set("silencingUser", true);
|
||||
|
||||
return ajax(`/admin/users/${this.id}/silence`, {
|
||||
type: "PUT",
|
||||
data,
|
||||
})
|
||||
.then((result) => this.setProperties(result.silence))
|
||||
.catch((e) => {
|
||||
const error = I18n.t("admin.user.silence_failed", {
|
||||
error: this._formatError(e),
|
||||
});
|
||||
bootbox.alert(error);
|
||||
})
|
||||
.finally(() => this.set("silencingUser", false));
|
||||
},
|
||||
|
||||
|
||||
@ -20,7 +20,10 @@ const ColorScheme = EmberObject.extend({
|
||||
},
|
||||
|
||||
startTrackingChanges() {
|
||||
this.set("originals", { name: this.name });
|
||||
this.set("originals", {
|
||||
name: this.name,
|
||||
user_selectable: this.user_selectable,
|
||||
});
|
||||
},
|
||||
|
||||
schemeJson() {
|
||||
@ -46,14 +49,22 @@ const ColorScheme = EmberObject.extend({
|
||||
return newScheme;
|
||||
},
|
||||
|
||||
@discourseComputed("name", "colors.@each.changed", "saving")
|
||||
changed(name) {
|
||||
@discourseComputed(
|
||||
"name",
|
||||
"user_selectable",
|
||||
"colors.@each.changed",
|
||||
"saving"
|
||||
)
|
||||
changed(name, user_selectable) {
|
||||
if (!this.originals) {
|
||||
return false;
|
||||
}
|
||||
if (this.originals.name !== name) {
|
||||
return true;
|
||||
}
|
||||
if (this.originals.user_selectable !== user_selectable) {
|
||||
return true;
|
||||
}
|
||||
if (this.colors.any((c) => c.get("changed"))) {
|
||||
return true;
|
||||
}
|
||||
@ -80,9 +91,9 @@ const ColorScheme = EmberObject.extend({
|
||||
this.setProperties({ savingStatus: I18n.t("saving"), saving: true });
|
||||
|
||||
const data = {};
|
||||
|
||||
if (!opts || !opts.enabledOnly) {
|
||||
data.name = this.name;
|
||||
data.user_selectable = this.user_selectable;
|
||||
data.base_scheme_id = this.base_scheme_id;
|
||||
data.colors = [];
|
||||
this.colors.forEach((c) => {
|
||||
|
||||
@ -2,8 +2,21 @@ import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Setting from "admin/mixins/setting-object";
|
||||
import EmberObject from "@ember/object";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
const SiteSetting = EmberObject.extend(Setting, {});
|
||||
const SiteSetting = EmberObject.extend(Setting, {
|
||||
@discourseComputed("setting")
|
||||
staffLogFilter(setting) {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
subject: setting,
|
||||
action_name: "change_site_setting",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
SiteSetting.reopenClass({
|
||||
findAll() {
|
||||
|
||||
@ -5,11 +5,6 @@ import { or, gt } from "@ember/object/computed";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const FIELDS_IDS = [0, 1, 5];
|
||||
@ -23,7 +18,6 @@ const Theme = RestModel.extend({
|
||||
isPendingUpdates: gt("remote_theme.commits_behind", 0),
|
||||
hasEditedFields: gt("editedFields.length", 0),
|
||||
hasParents: gt("parent_themes.length", 0),
|
||||
diffLocalChangesUrl: url("id", "/admin/themes/%@/diff_local_changes"),
|
||||
|
||||
@discourseComputed("theme_fields.[]")
|
||||
targets() {
|
||||
@ -292,37 +286,9 @@ const Theme = RestModel.extend({
|
||||
},
|
||||
|
||||
updateToLatest() {
|
||||
return ajax(this.diffLocalChangesUrl).then((json) => {
|
||||
if (json && json.error) {
|
||||
bootbox.alert(
|
||||
I18n.t("generic_error_with_reason", {
|
||||
error: json.error,
|
||||
})
|
||||
);
|
||||
} else if (json && json.diff) {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.theme.update_confirm") +
|
||||
`<pre><code class="diff">${escapeExpression(
|
||||
json.diff
|
||||
)}</code></pre>`,
|
||||
I18n.t("cancel"),
|
||||
I18n.t("admin.customize.theme.update_confirm_yes"),
|
||||
(result) => {
|
||||
if (result) {
|
||||
return this.save({ remote_update: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
// TODO: Models shouldn't be updating the DOM
|
||||
highlightSyntax(undefined, this.siteSettings, this.session);
|
||||
} else {
|
||||
return this.save({ remote_update: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
}
|
||||
});
|
||||
return this.save({ remote_update: true }).then(() =>
|
||||
this.set("changed", false)
|
||||
);
|
||||
},
|
||||
|
||||
changed: false,
|
||||
|
||||
@ -28,6 +28,10 @@ export default Route.extend({
|
||||
const fields = wrapper.model
|
||||
.get("fields")
|
||||
[wrapper.target].map((f) => f.name);
|
||||
if (wrapper.model.remote_theme) {
|
||||
this.transitionTo("adminCustomizeThemes.index");
|
||||
return;
|
||||
}
|
||||
if (!fields.includes(wrapper.field_name)) {
|
||||
this.transitionTo(
|
||||
"adminCustomizeThemes.edit",
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
<div class="setting-label">
|
||||
<h3>{{settingName}}</h3>
|
||||
<h3>
|
||||
{{#if staffLogFilter}}
|
||||
{{settingName}}
|
||||
{{#link-to "adminLogs.staffActionLogs" (query-params filters=staffLogFilter) title=(i18n "admin.settings.history")}}
|
||||
<span class="history-icon">
|
||||
{{d-icon "history"}}
|
||||
</span>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{settingName}}
|
||||
{{/if}}
|
||||
</h3>
|
||||
{{#if defaultIsAvailable}}
|
||||
<a href onClick={{action "setDefaultValues"}}>{{setting.setDefaultValuesLabel}}</a>
|
||||
{{/if}}
|
||||
|
||||
@ -8,10 +8,20 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</label>
|
||||
{{text-field
|
||||
value=reason
|
||||
<label>
|
||||
{{i18n "admin.user.suspend_reason_title"}}
|
||||
</label>
|
||||
{{combo-box
|
||||
content=reasons
|
||||
value=selectedReason
|
||||
class="suspend-reason"
|
||||
placeholderKey="admin.user.suspend_reason_placeholder"}}
|
||||
onChange=(action setSelectedReason)}}
|
||||
{{#if isCustomReason}}
|
||||
{{text-field
|
||||
value=customReason
|
||||
class="suspend-reason"
|
||||
onChange=(action setCustomReason)}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
|
||||
@ -41,7 +41,14 @@
|
||||
<br>
|
||||
|
||||
<div class="admin-controls">
|
||||
{{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.color_scheme_user_selectable" checked=model.user_selectable}}
|
||||
{{#if model.theme_id}}
|
||||
{{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.color_scheme_user_selectable" checked=model.user_selectable}}
|
||||
{{else}}
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.user_selectable}}
|
||||
{{i18n "admin.customize.theme.color_scheme_user_selectable"}}
|
||||
</label>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<div class="show-current-style">
|
||||
{{plugin-outlet name="admin-customize-themes-show-top" args=(hash theme=model)}}
|
||||
<div class="title">
|
||||
{{#if editingName}}
|
||||
{{text-field value=model.name autofocus="true"}}
|
||||
@ -59,7 +60,12 @@
|
||||
{{#if sourceIsHttp}}
|
||||
<a class="remote-url" href={{remoteThemeLink}}>{{i18n "admin.customize.theme.source_url"}}{{d-icon "link"}}</a>
|
||||
{{else}}
|
||||
<div class="remote-url"><code>{{model.remote_theme.remote_url}}</code></div>
|
||||
<div class="remote-url">
|
||||
<code>{{model.remote_theme.remote_url}}</code>
|
||||
{{#if model.remote_theme.branch}}
|
||||
(<code>{{model.remote_theme.branch}}</code>)
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if model.remote_theme.about_url}}
|
||||
@ -128,12 +134,17 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#unless model.component}}
|
||||
{{#if showCheckboxes}}
|
||||
<div class="control-unit">
|
||||
{{inline-edit-checkbox action=(action "applyDefault") labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
{{#unless model.component}}
|
||||
{{inline-edit-checkbox action=(action "applyDefault") labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
{{/unless}}
|
||||
{{#if model.remote_theme}}
|
||||
{{inline-edit-checkbox action=(action "applyAutoUpdateable") labelKey="admin.customize.theme.auto_update" checked=model.auto_update}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless model.component}}
|
||||
{{#d-section class="form-horizontal theme settings control-unit"}}
|
||||
@ -186,45 +197,47 @@
|
||||
{{/d-section}}
|
||||
{{/if}}
|
||||
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.css_html"}}</div>
|
||||
{{#if model.hasEditedFields}}
|
||||
<div class="description">{{i18n "admin.customize.theme.custom_sections"}}</div>
|
||||
<ul>
|
||||
{{#each editedFieldsFormatted as |field|}}
|
||||
<li>{{field}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#unless model.remote_theme}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.css_html"}}</div>
|
||||
{{#if model.hasEditedFields}}
|
||||
<div class="description">{{i18n "admin.customize.theme.custom_sections"}}</div>
|
||||
<ul>
|
||||
{{#each editedFieldsFormatted as |field|}}
|
||||
<li>{{field}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{d-button
|
||||
class="btn-default edit"
|
||||
action=(action "editTheme")
|
||||
label="admin.customize.theme.edit_css_html"}}
|
||||
</div>
|
||||
{{d-button
|
||||
class="btn-default edit"
|
||||
action=(action "editTheme")
|
||||
label="admin.customize.theme.edit_css_html"}}
|
||||
</div>
|
||||
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.uploads"}}</div>
|
||||
{{#if model.uploads}}
|
||||
<ul class="removable-list">
|
||||
{{#each model.uploads as |upload|}}
|
||||
<li>
|
||||
<span class="col">${{upload.name}}: <a href={{upload.url}} rel="noopener noreferrer" target="_blank">{{upload.filename}}</a></span>
|
||||
<span class="col">
|
||||
{{d-button action=(action "removeUpload") actionParam=upload class="second btn-default btn-default cancel-edit" icon="times"}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">{{i18n "admin.customize.theme.no_uploads"}}</div>
|
||||
{{/if}}
|
||||
{{d-button action=(action "addUploadModal") class="btn-default" icon="plus" label="admin.customize.theme.add"}}
|
||||
</div>
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.uploads"}}</div>
|
||||
{{#if model.uploads}}
|
||||
<ul class="removable-list">
|
||||
{{#each model.uploads as |upload|}}
|
||||
<li>
|
||||
<span class="col">${{upload.name}}: <a href={{upload.url}} rel="noopener noreferrer" target="_blank">{{upload.filename}}</a></span>
|
||||
<span class="col">
|
||||
{{d-button action=(action "removeUpload") actionParam=upload class="second btn-default btn-default cancel-edit" icon="times"}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">{{i18n "admin.customize.theme.no_uploads"}}</div>
|
||||
{{/if}}
|
||||
{{d-button action=(action "addUploadModal") class="btn-default" icon="plus" label="admin.customize.theme.add"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if extraFiles.length}}
|
||||
<div class="control-unit">
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
<td><a href="mailto:{{l.to_address}}">{{l.to_address}}</a></td>
|
||||
<td class="email-address"><a href="mailto:{{l.to_address}}">{{l.to_address}}</a></td>
|
||||
{{#if l.has_bounce_key}}
|
||||
<td><a href {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
|
||||
{{else}}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
<td><a href="mailto:{{l.to_address}}">{{l.to_address}}</a></td>
|
||||
<td class="email-address"><a href="mailto:{{l.to_address}}">{{l.to_address}}</a></td>
|
||||
<td>{{l.email_type}}</td>
|
||||
<td>
|
||||
{{#if l.post_url}}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{{#d-modal-body title="admin.user.silence_modal_title"}}
|
||||
{{#conditional-loading-spinner condition=loadingUser}}
|
||||
|
||||
{{#if errorMessage}}
|
||||
<div class="alert alert-error">{{errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="until-controls">
|
||||
<label>
|
||||
{{future-date-input
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{{#d-modal-body title="admin.user.suspend_modal_title"}}
|
||||
{{#conditional-loading-spinner condition=loadingUser}}
|
||||
|
||||
{{#if errorMessage}}
|
||||
<div class="alert alert-error">{{errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if user.canSuspend}}
|
||||
<div class="until-controls">
|
||||
<label>
|
||||
|
||||
@ -671,10 +671,19 @@
|
||||
<div class="field">{{i18n "admin.user.sso.external_name"}}</div>
|
||||
<div class="value">{{sso.external_name}}</div>
|
||||
</div>
|
||||
{{#if sso.external_email}}
|
||||
{{#if canAdminCheckEmails}}
|
||||
<div class="display-row">
|
||||
<div class="field">{{i18n "admin.user.sso.external_email"}}</div>
|
||||
<div class="value">{{sso.external_email}}</div>
|
||||
{{#if ssoExternalEmail}}
|
||||
<div class="value">{{ssoExternalEmail}}</div>
|
||||
{{else}}
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "checkSsoEmail")
|
||||
actionParam=model icon="far-envelope"
|
||||
label="admin.users.check_email.text"
|
||||
title="admin.users.check_email.title"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="display-row">
|
||||
|
||||
75
app/assets/javascripts/browser-update.js
Normal file
@ -0,0 +1,75 @@
|
||||
//browser-update.org notification script, <browser-update.org>
|
||||
//Copyright (c) 2007-2009, MIT Style License <browser-update.org/LICENSE.txt>
|
||||
|
||||
(function () {
|
||||
var $buo = function () {
|
||||
// Sometimes we have to resort to parsing the user agent string. :(
|
||||
if (navigator && navigator.userAgent) {
|
||||
var ua = navigator.userAgent;
|
||||
|
||||
// we don't ask Googlebot to update their browser
|
||||
if (
|
||||
ua.indexOf("Googlebot") >= 0 ||
|
||||
ua.indexOf("Mediapartners") >= 0 ||
|
||||
ua.indexOf("AdsBot") >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.unsupportedBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementsByTagName("body")[0].className += " crawler";
|
||||
var mainElement = document.getElementById("main");
|
||||
var noscriptElements = document.getElementsByTagName("noscript");
|
||||
// find the element with the "data-path" attribute set
|
||||
for (var i = 0; i < noscriptElements.length; ++i) {
|
||||
if (noscriptElements[i].getAttribute("data-path")) {
|
||||
// noscriptElements[i].innerHTML contains encoded HTML
|
||||
if (noscriptElements[i].childNodes.length > 0) {
|
||||
mainElement.innerHTML = noscriptElements[i].childNodes[0].nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve localized browser upgrade text
|
||||
var t = I18n.t("browser_update"); // eslint-disable-line no-undef
|
||||
if (t.indexOf(".browser_update]") !== -1) {
|
||||
// very old browsers might fail to load even translations
|
||||
t =
|
||||
'Unfortunately, <a href="https://www.discourse.org/faq/#browser">your browser is too old to work on this site</a>. Please <a href="https://browsehappy.com">upgrade your browser</a> to view rich content, log in and reply.';
|
||||
}
|
||||
|
||||
// create the notification div HTML
|
||||
var div = document.createElement("div");
|
||||
div.className = "buorg";
|
||||
div.innerHTML = "<div>" + t + "</div>";
|
||||
|
||||
// create the notification div stylesheet
|
||||
var sheet = document.createElement("style");
|
||||
var style =
|
||||
".buorg {position:absolute; z-index:111111; width:100%; top:0px; left:0px; background:#FDF2AB; text-align:left; font-family: sans-serif; color:#000; font-size: 14px;} .buorg div {padding: 8px;} .buorg a, .buorg a:visited {color:#E25600; text-decoration: underline;} @media print { .buorg { display: none !important; } }";
|
||||
|
||||
// insert the div and stylesheet into the DOM
|
||||
document.body.appendChild(div); // put it last in the DOM so Googlebot doesn't include it in search excerpts
|
||||
document.getElementsByTagName("head")[0].appendChild(sheet);
|
||||
try {
|
||||
sheet.innerText = style;
|
||||
sheet.innerHTML = style;
|
||||
} catch (e) {
|
||||
try {
|
||||
sheet.styleSheet.cssText = style;
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// shift the body down to make room for our notification div
|
||||
document.body.style.marginTop = div.clientHeight + "px";
|
||||
};
|
||||
|
||||
$bu = $buo(); // eslint-disable-line no-undef
|
||||
})(this);
|
||||
@ -52,6 +52,24 @@ export function buildResolver(baseName) {
|
||||
return "service:app-events";
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries({
|
||||
"controller:discovery.categoryWithID": "controller:discovery.category",
|
||||
"controller:discovery.parentCategory": "controller:discovery.category",
|
||||
"controller:tags-show": "controller:tag-show",
|
||||
"controller:tags.show": "controller:tag.show",
|
||||
"controller:tagsShow": "controller:tagShow",
|
||||
"route:discovery.categoryWithID": "route:discovery.category",
|
||||
"route:discovery.parentCategory": "route:discovery.category",
|
||||
"route:tags-show": "route:tag-show",
|
||||
"route:tags.show": "route:tag.show",
|
||||
"route:tagsShow": "route:tagShow",
|
||||
})) {
|
||||
if (fullName === key) {
|
||||
deprecated(`${key} was replaced with ${value}`, { since: "2.6.0" });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const split = fullName.split(":");
|
||||
if (split.length > 1) {
|
||||
const appBase = `${baseName}/${split[0]}s/`;
|
||||
|
||||
7
app/assets/javascripts/discourse/app/adapters/tag.js
Normal file
@ -0,0 +1,7 @@
|
||||
import RESTAdapter from "discourse/adapters/rest";
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
pathFor(store, type, id) {
|
||||
return id ? `/tag/${id}` : `/tags`;
|
||||
},
|
||||
});
|
||||
@ -313,6 +313,10 @@ export default Component.extend({
|
||||
this.appEvents.on("composer:replace-text", this, "_replaceText");
|
||||
}
|
||||
this._mouseTrap = mouseTrap;
|
||||
|
||||
if (isTesting()) {
|
||||
this.element.addEventListener("paste", this.paste.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
_insertBlock(text) {
|
||||
@ -336,6 +340,10 @@ export default Component.extend({
|
||||
mouseTrap.unbind(sc)
|
||||
);
|
||||
$(this.element.querySelector(".d-editor-preview")).off("click.preview");
|
||||
|
||||
if (isTesting()) {
|
||||
this.element.removeEventListener("paste", this.paste);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
@ -870,7 +878,7 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
paste(e) {
|
||||
if (!$(".d-editor-input").is(":focus")) {
|
||||
if (!$(".d-editor-input").is(":focus") && !isTesting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -894,7 +902,7 @@ export default Component.extend({
|
||||
!isInlinePasting &&
|
||||
!isCodeBlock
|
||||
) {
|
||||
plainText = plainText.trim().replace(/\r/g, "");
|
||||
plainText = plainText.replace(/\r/g, "");
|
||||
const table = this._extractTable(plainText);
|
||||
if (table) {
|
||||
this.appEvents.trigger("composer:insert-text", table);
|
||||
|
||||
@ -10,6 +10,13 @@ export default Component.extend(FilterModeMixin, {
|
||||
|
||||
tagName: "",
|
||||
|
||||
// Should be a `readOnly` instead but some themes/plugins still pass
|
||||
// the `categories` property into this component
|
||||
@discourseComputed("site.categoriesList")
|
||||
categories(categoriesList) {
|
||||
return categoriesList;
|
||||
},
|
||||
|
||||
@discourseComputed("category")
|
||||
showCategoryNotifications(category) {
|
||||
return category && this.currentUser;
|
||||
@ -25,14 +32,20 @@ export default Component.extend(FilterModeMixin, {
|
||||
@discourseComputed(
|
||||
"createTopicDisabled",
|
||||
"hasDraft",
|
||||
"categoryReadOnlyBanner"
|
||||
"categoryReadOnlyBanner",
|
||||
"canCreateTopicOnTag",
|
||||
"tag.id"
|
||||
)
|
||||
createTopicButtonDisabled(
|
||||
createTopicDisabled,
|
||||
hasDraft,
|
||||
categoryReadOnlyBanner
|
||||
categoryReadOnlyBanner,
|
||||
canCreateTopicOnTag,
|
||||
tagId
|
||||
) {
|
||||
if (categoryReadOnlyBanner && !hasDraft) {
|
||||
if (tagId && !canCreateTopicOnTag) {
|
||||
return true;
|
||||
} else if (categoryReadOnlyBanner && !hasDraft) {
|
||||
return false;
|
||||
}
|
||||
return createTopicDisabled;
|
||||
@ -47,11 +60,6 @@ export default Component.extend(FilterModeMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
categories() {
|
||||
return this.site.get("categoriesList");
|
||||
},
|
||||
|
||||
@discourseComputed("hasDraft")
|
||||
createTopicLabel(hasDraft) {
|
||||
return hasDraft ? "topic.open_draft" : "topic.create";
|
||||
@ -60,14 +68,20 @@ export default Component.extend(FilterModeMixin, {
|
||||
@discourseComputed("category.can_edit")
|
||||
showCategoryEdit: (canEdit) => canEdit,
|
||||
|
||||
@discourseComputed("filterType", "category", "noSubcategories")
|
||||
navItems(filterType, category, noSubcategories) {
|
||||
@discourseComputed("additionalTags", "category", "tag.id")
|
||||
showToggleInfo(additionalTags, category, tagId) {
|
||||
return !additionalTags && !category && tagId !== "none";
|
||||
},
|
||||
|
||||
@discourseComputed("filterType", "category", "noSubcategories", "tag.id")
|
||||
navItems(filterType, category, noSubcategories, tagId) {
|
||||
const currentRouteQueryParams = this.get("router.currentRoute.queryParams");
|
||||
|
||||
return NavItem.buildList(category, {
|
||||
filterType,
|
||||
noSubcategories,
|
||||
currentRouteQueryParams,
|
||||
tagId,
|
||||
siteSettings: this.siteSettings,
|
||||
});
|
||||
},
|
||||
|
||||
@ -2,6 +2,6 @@ import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
$(this.element.querySelector("input")).select().focus();
|
||||
$(this.element.querySelector(".invite-link-input")).select().focus();
|
||||
},
|
||||
});
|
||||
|
||||
@ -19,9 +19,19 @@ export default Component.extend({
|
||||
editing: false,
|
||||
_updates: null,
|
||||
|
||||
@discourseComputed("reviewable.type")
|
||||
customClass(type) {
|
||||
return type.dasherize();
|
||||
@discourseComputed(
|
||||
"reviewable.type",
|
||||
"siteSettings.blur_tl0_flagged_posts_media",
|
||||
"reviewable.target_created_by_trust_level"
|
||||
)
|
||||
customClasses(type, blurEnabled, trustLevel) {
|
||||
let classes = type.dasherize();
|
||||
|
||||
if (blurEnabled && trustLevel === 0) {
|
||||
classes = `${classes} blur-images`;
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
|
||||
@ -99,14 +99,18 @@ export default Component.extend(bufferedProperty("model"), {
|
||||
},
|
||||
|
||||
setPermissionsGroups(groupIds) {
|
||||
let permissions = {};
|
||||
let updatedPermissions = Object.assign(
|
||||
{},
|
||||
this.buffered.get("permissions")
|
||||
);
|
||||
|
||||
this.allGroups.forEach((group) => {
|
||||
if (groupIds.includes(group.id)) {
|
||||
permissions[group.name] = PermissionType.FULL;
|
||||
updatedPermissions[group.name] = PermissionType.FULL;
|
||||
}
|
||||
});
|
||||
|
||||
this.buffered.set("permissions", permissions);
|
||||
this.buffered.set("permissions", updatedPermissions);
|
||||
},
|
||||
|
||||
save() {
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
});
|
||||
@ -3,6 +3,7 @@ import MountWidget from "discourse/components/mount-widget";
|
||||
import Docking from "discourse/mixins/docking";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import optionalService from "discourse/lib/optional-service";
|
||||
import outletHeights from "discourse/lib/header-outlet-height";
|
||||
|
||||
const headerPadding = () => {
|
||||
let topPadding = parseInt($("#main-outlet").css("padding-top"), 10) + 3;
|
||||
@ -67,13 +68,16 @@ export default MountWidget.extend(Docking, {
|
||||
|
||||
const prev = this.dockAt;
|
||||
const posTop = headerPadding() + info.offset();
|
||||
const pos = posTop + timelineHeight;
|
||||
const pos = posTop + timelineHeight - outletHeights();
|
||||
|
||||
this.dockBottom = false;
|
||||
if (posTop < topicTop) {
|
||||
this.dockAt = parseInt(topicTop, 10);
|
||||
} else if (pos > topicBottom + footerHeight) {
|
||||
this.dockAt = parseInt(topicBottom - timelineHeight + footerHeight, 10);
|
||||
this.dockAt = parseInt(
|
||||
topicBottom - timelineHeight + footerHeight + outletHeights(),
|
||||
10
|
||||
);
|
||||
this.dockBottom = true;
|
||||
if (this.dockAt < 0) {
|
||||
this.dockAt = 0;
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
import Component from "@ember/component";
|
||||
import { propertyEqual } from "discourse/lib/computed";
|
||||
import { computed } from "@ember/object";
|
||||
import { actionDescription } from "discourse/widgets/post-small-action";
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: [
|
||||
":user-stream-item",
|
||||
":item", // DEPRECATED: 'item' class
|
||||
"item.hidden",
|
||||
"hidden",
|
||||
"item.deleted:deleted",
|
||||
"moderatorAction",
|
||||
],
|
||||
|
||||
hidden: computed("item.hidden", function () {
|
||||
return (
|
||||
this.get("item.hidden") && !(this.currentUser && this.currentUser.staff)
|
||||
);
|
||||
}),
|
||||
moderatorAction: propertyEqual(
|
||||
"item.post_type",
|
||||
"site.post_types.moderator_action"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import I18n from "I18n";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import Component from "@ember/component";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
@ -9,6 +10,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { on } from "@ember/object/evented";
|
||||
import bootbox from "bootbox";
|
||||
|
||||
export default Component.extend(LoadMore, {
|
||||
_initialize: on("init", function () {
|
||||
@ -94,13 +96,22 @@ export default Component.extend(LoadMore, {
|
||||
|
||||
removeDraft(draft) {
|
||||
const stream = this.stream;
|
||||
Draft.clear(draft.draft_key, draft.sequence)
|
||||
.then(() => {
|
||||
stream.remove(draft);
|
||||
})
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
bootbox.confirm(
|
||||
I18n.t("drafts.remove_confirmation"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
Draft.clear(draft.draft_key, draft.sequence)
|
||||
.then(() => {
|
||||
stream.remove(draft);
|
||||
})
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import Controller from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { cookAsync } from "discourse/lib/text";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
post: null,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
|
||||
notice: null,
|
||||
saving: false,
|
||||
|
||||
@discourseComputed("saving", "notice")
|
||||
disabled(saving, notice) {
|
||||
return saving || isEmpty(notice);
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
notice: "",
|
||||
saving: false,
|
||||
});
|
||||
},
|
||||
|
||||
onClose() {
|
||||
const reject = this.reject;
|
||||
if (reject) {
|
||||
reject();
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setNotice() {
|
||||
this.set("saving", true);
|
||||
|
||||
const post = this.post;
|
||||
const resolve = this.resolve;
|
||||
const reject = this.reject;
|
||||
const notice = this.notice;
|
||||
|
||||
// Let `updatePostField` handle state.
|
||||
this.setProperties({ resolve: null, reject: null });
|
||||
|
||||
post
|
||||
.updatePostField("notice", notice)
|
||||
.then(() => cookAsync(notice, { features: { onebox: false } }))
|
||||
.then((cookedNotice) => {
|
||||
post.setProperties({
|
||||
notice_type: "custom",
|
||||
notice_args: cookedNotice.string,
|
||||
});
|
||||
resolve();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
this.send("closeModal");
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { cookAsync } from "discourse/lib/text";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
post: null,
|
||||
resolve: null,
|
||||
reject: null,
|
||||
|
||||
notice: null,
|
||||
saving: false,
|
||||
|
||||
@discourseComputed("saving", "notice")
|
||||
disabled(saving, notice) {
|
||||
return saving || isEmpty(notice);
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setProperties({ notice: "", saving: false });
|
||||
},
|
||||
|
||||
onClose() {
|
||||
if (this.reject) {
|
||||
this.reject();
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
setNotice(notice) {
|
||||
const { resolve, reject } = this;
|
||||
this.setProperties({ saving: true, resolve: null, reject: null });
|
||||
|
||||
this.model
|
||||
.updatePostField("notice", notice)
|
||||
.then(() => {
|
||||
if (notice) {
|
||||
return cookAsync(notice, { features: { onebox: false } });
|
||||
}
|
||||
})
|
||||
.then((cooked) =>
|
||||
this.model.set(
|
||||
"notice",
|
||||
cooked
|
||||
? {
|
||||
type: "custom",
|
||||
raw: notice,
|
||||
cooked: cooked.string,
|
||||
}
|
||||
: null
|
||||
)
|
||||
)
|
||||
.then(resolve, reject)
|
||||
.finally(() => this.send("closeModal"));
|
||||
},
|
||||
});
|
||||
@ -28,6 +28,7 @@ import { isTesting } from "discourse-common/config/environment";
|
||||
import EmberObject, { computed, action } from "@ember/object";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import bootbox from "bootbox";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import {
|
||||
cannotPostAgain,
|
||||
durationTextFromSeconds,
|
||||
@ -1097,46 +1098,36 @@ export default Controller.extend({
|
||||
cancel(this._saveDraftDebounce);
|
||||
}
|
||||
|
||||
const keyPrefix =
|
||||
this.model.action === "edit" ? "post.abandon_edit" : "post.abandon";
|
||||
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
|
||||
bootbox.dialog(I18n.t(keyPrefix + ".confirm"), [
|
||||
{
|
||||
label: differentDraft
|
||||
? I18n.t(keyPrefix + ".no_save_draft")
|
||||
: I18n.t(keyPrefix + ".no_value"),
|
||||
callback: () => {
|
||||
// cancel composer without destroying draft on new draft context
|
||||
if (differentDraft) {
|
||||
const controller = showModal("discard-draft", {
|
||||
model: this.model,
|
||||
modalClass: "discard-draft-modal",
|
||||
title: "post.abandon.title",
|
||||
});
|
||||
controller.setProperties({
|
||||
differentDraft,
|
||||
onDestroyDraft: () => {
|
||||
this.destroyDraft()
|
||||
.then(() => {
|
||||
this.model.clearState();
|
||||
this.close();
|
||||
})
|
||||
.finally(() => {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
onSaveDraft: () => {
|
||||
// cancel composer without destroying draft on new draft context
|
||||
if (differentDraft) {
|
||||
this.model.clearState();
|
||||
this.close();
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject();
|
||||
},
|
||||
reject();
|
||||
},
|
||||
{
|
||||
label: I18n.t(keyPrefix + ".yes_value"),
|
||||
class: "btn-danger",
|
||||
callback: (result) => {
|
||||
if (result) {
|
||||
this.destroyDraft()
|
||||
.then(() => {
|
||||
this.model.clearState();
|
||||
this.close();
|
||||
})
|
||||
.finally(() => {
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
} else {
|
||||
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
||||
this.destroyDraft()
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import Controller from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
differentDraft: null,
|
||||
|
||||
@discourseComputed()
|
||||
keyPrefix() {
|
||||
return this.model.action === "edit" ? "post.abandon_edit" : "post.abandon";
|
||||
},
|
||||
|
||||
@discourseComputed("keyPrefix")
|
||||
descriptionKey(keyPrefix) {
|
||||
return `${keyPrefix}.confirm`;
|
||||
},
|
||||
|
||||
@discourseComputed("keyPrefix")
|
||||
discardKey(keyPrefix) {
|
||||
return `${keyPrefix}.yes_value`;
|
||||
},
|
||||
|
||||
@discourseComputed("keyPrefix", "differentDraft")
|
||||
saveKey(keyPrefix, differentDraft) {
|
||||
return differentDraft
|
||||
? `${keyPrefix}.no_save_draft`
|
||||
: `${keyPrefix}.no_value`;
|
||||
},
|
||||
|
||||
actions: {
|
||||
_destroyDraft() {
|
||||
this.onDestroyDraft();
|
||||
this.send("closeModal");
|
||||
},
|
||||
_saveDraft() {
|
||||
this.onSaveDraft();
|
||||
this.send("closeModal");
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { alias, not } from "@ember/object/computed";
|
||||
import { alias, not, equal } from "@ember/object/computed";
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Category from "discourse/models/category";
|
||||
@ -10,6 +10,10 @@ export default Controller.extend({
|
||||
navigationCategory: controller("navigation/category"),
|
||||
application: controller(),
|
||||
router: service(),
|
||||
viewingCategoriesList: equal(
|
||||
"router.currentRouteName",
|
||||
"discovery.categories"
|
||||
),
|
||||
|
||||
loading: false,
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import bootbox from "bootbox";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
import PermissionType from "discourse/models/permission-type";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
|
||||
export default Controller.extend({
|
||||
selectedTab: "general",
|
||||
@ -93,8 +95,11 @@ export default Controller.extend({
|
||||
model.setProperties({
|
||||
slug: result.category.slug,
|
||||
id: result.category.id,
|
||||
createdCategory: true,
|
||||
can_edit: result.category.can_edit,
|
||||
permission: PermissionType.FULL,
|
||||
notification_level: NotificationLevels.REGULAR,
|
||||
});
|
||||
this.site.updateCategory(model);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -111,15 +116,17 @@ export default Controller.extend({
|
||||
I18n.t("yes_value"),
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.model.destroy().then(
|
||||
() => {
|
||||
this.model
|
||||
.destroy()
|
||||
.then(() => {
|
||||
this.transitionToRoute("discovery.categories");
|
||||
},
|
||||
() => {
|
||||
})
|
||||
.catch(() => {
|
||||
this.displayErrors([I18n.t("category.delete_error")]);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("deleting", false);
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.set("deleting", false);
|
||||
}
|
||||
@ -132,15 +139,7 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
goBack() {
|
||||
if (this.model.createdCategory) {
|
||||
DiscourseURL.redirectTo(this.model.url);
|
||||
} else {
|
||||
DiscourseURL.routeTo(this.model.url);
|
||||
}
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
this.toggleProperty("expandedMenu");
|
||||
DiscourseURL.routeTo(this.model.url);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,7 +9,6 @@ export default Controller.extend({
|
||||
|
||||
this.saveAttrNames = [
|
||||
"muted_usernames",
|
||||
"ignored_usernames",
|
||||
"new_topic_duration_minutes",
|
||||
"auto_track_topics_after_msecs",
|
||||
"notification_level_when_replying",
|
||||
|
||||
@ -43,7 +43,6 @@ export default Controller.extend({
|
||||
|
||||
this.saveAttrNames = [
|
||||
"muted_usernames",
|
||||
"ignored_usernames",
|
||||
"allowed_pm_usernames",
|
||||
"enable_allowed_pm_users",
|
||||
];
|
||||
|
||||
@ -26,7 +26,7 @@ export default Controller.extend(ModalFunctionality, BufferedContent, {
|
||||
this.send("closeModal");
|
||||
|
||||
if (result.responseJson.tag) {
|
||||
this.transitionToRoute("tags.show", result.responseJson.tag.id);
|
||||
this.transitionToRoute("tag.show", result.responseJson.tag.id);
|
||||
} else {
|
||||
this.flash(extractError(result.responseJson.errors[0]), "error");
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import Controller from "@ember/controller";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Controller.extend({
|
||||
saving: false,
|
||||
@ -9,7 +11,7 @@ export default Controller.extend({
|
||||
actions: {
|
||||
save() {
|
||||
let priorities = {};
|
||||
this.get("settings.reviewable_score_types").forEach((st) => {
|
||||
this.scoreTypes.forEach((st) => {
|
||||
priorities[st.id] = parseFloat(st.reviewable_priority);
|
||||
});
|
||||
|
||||
@ -25,4 +27,15 @@ export default Controller.extend({
|
||||
.finally(() => this.set("saving", false));
|
||||
},
|
||||
},
|
||||
|
||||
@discourseComputed("settings.reviewable_score_types")
|
||||
scoreTypes(types) {
|
||||
const username = I18n.t("review.example_username");
|
||||
|
||||
return types.map((type) =>
|
||||
Object.assign({}, type, {
|
||||
title: type.title.replace("%{username}", username),
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -28,8 +28,6 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
|
||||
q: null,
|
||||
showInfo: false,
|
||||
|
||||
categories: alias("site.categoriesList"),
|
||||
|
||||
@discourseComputed("list", "list.draft")
|
||||
createTopicLabel(list, listDraft) {
|
||||
return listDraft ? "topic.open_draft" : "topic.create";
|
||||
@ -73,11 +71,6 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
|
||||
return this.siteSettings.show_filter_by_tag;
|
||||
},
|
||||
|
||||
@discourseComputed("additionalTags", "category", "tag.id")
|
||||
showToggleInfo(additionalTags, category, tagId) {
|
||||
return !additionalTags && !category && tagId !== "none";
|
||||
},
|
||||
|
||||
loadMoreTopics() {
|
||||
return this.list.loadMore();
|
||||
},
|
||||
@ -3,7 +3,6 @@ import { empty, alias } from "@ember/object/computed";
|
||||
import Controller from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import Topic from "discourse/models/topic";
|
||||
import Category from "discourse/models/category";
|
||||
import bootbox from "bootbox";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
@ -241,11 +240,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||
|
||||
changeCategory() {
|
||||
const categoryId = parseInt(this.newCategoryId, 10) || 0;
|
||||
const category = Category.findById(categoryId);
|
||||
|
||||
this.perform({ type: "change_category", category_id: categoryId }).then(
|
||||
(topics) => {
|
||||
topics.forEach((t) => t.set("category", category));
|
||||
topics.forEach((t) => t.set("category_id", categoryId));
|
||||
(this.refreshClosure || identity)();
|
||||
this.send("closeModal");
|
||||
}
|
||||
|
||||
@ -857,22 +857,17 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
this.send("showGrantBadgeModal");
|
||||
},
|
||||
|
||||
addNotice(post) {
|
||||
changeNotice(post) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const modal = showModal("add-post-notice");
|
||||
modal.setProperties({ post, resolve, reject });
|
||||
const modal = showModal("change-post-notice", { model: post });
|
||||
modal.setProperties({
|
||||
resolve,
|
||||
reject,
|
||||
notice: post.notice ? post.notice.raw : "",
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeNotice(post) {
|
||||
return post.updatePostField("notice", null).then(() =>
|
||||
post.setProperties({
|
||||
notice_type: null,
|
||||
notice_args: null,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
toggleParticipant(user) {
|
||||
this.get("model.postStream")
|
||||
.toggleParticipant(user.get("username"))
|
||||
@ -1424,6 +1419,12 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "destroyed": {
|
||||
postStream
|
||||
.triggerDestroyedPost(data.id)
|
||||
.then(() => refresh({ id: data.id }));
|
||||
break;
|
||||
}
|
||||
case "recovered": {
|
||||
postStream
|
||||
.triggerRecoveredPost(data.id)
|
||||
|
||||
@ -9,6 +9,6 @@ export default Controller.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.badgeSortOrder = ["badge.badge_type.sort_order", "badge.name"];
|
||||
this.badgeSortOrder = ["badge.badge_type.sort_order:desc", "badge.name"];
|
||||
},
|
||||
});
|
||||
|
||||
@ -36,8 +36,11 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// Also use Universal Analytics if it is present
|
||||
if (typeof window.ga !== "undefined") {
|
||||
// Use Universal Analytics v3 if it is present
|
||||
if (
|
||||
typeof window.ga !== "undefined" &&
|
||||
typeof window.gtag === "undefined"
|
||||
) {
|
||||
appEvents.on("page:changed", (data) => {
|
||||
if (!data.replacedOnlyQueryParams) {
|
||||
window.ga("send", "pageview", { page: data.url, title: data.title });
|
||||
@ -45,7 +48,19 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
// And Google Tag Manager too
|
||||
// And Universal Analytics v4 if we're upgraded
|
||||
if (typeof window.gtag !== "undefined") {
|
||||
appEvents.on("page:changed", (data) => {
|
||||
if (!data.replacedOnlyQueryParams) {
|
||||
window.gtag("event", "page_view", {
|
||||
page_location: data.url,
|
||||
page_title: data.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Google Tag Manager too
|
||||
if (typeof window.dataLayer !== "undefined") {
|
||||
appEvents.on("page:changed", (data) => {
|
||||
if (!data.replacedOnlyQueryParams) {
|
||||
|
||||
@ -16,7 +16,7 @@ export default {
|
||||
},
|
||||
updateAppBackground() {
|
||||
later(() => {
|
||||
const header = document.querySelectorAll(".d-header")[0];
|
||||
const header = document.querySelector(".d-header-wrap .d-header");
|
||||
if (header) {
|
||||
const styles = window.getComputedStyle(header);
|
||||
postRNWebviewMessage("headerBg", styles.backgroundColor);
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
export default function () {
|
||||
const outletSelector = [
|
||||
".above-site-header-outlet",
|
||||
".below-site-header-outlet",
|
||||
];
|
||||
// If these outlets have height they impact timeline and usercard positioning
|
||||
|
||||
let outletHeights = 0;
|
||||
|
||||
outletSelector.forEach(function (outletClass) {
|
||||
if (document.querySelector(outletClass)) {
|
||||
let outlets = document.querySelectorAll(outletClass);
|
||||
outlets.forEach((outlet) => {
|
||||
if (outlet.offsetHeight) {
|
||||
outletHeights += parseInt(outlet.offsetHeight, 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return outletHeights;
|
||||
}
|
||||
@ -118,6 +118,8 @@ export default {
|
||||
},
|
||||
|
||||
teardown() {
|
||||
this.keyTrapper.reset();
|
||||
this.keyTrapper = null;
|
||||
this.container = null;
|
||||
},
|
||||
|
||||
|
||||
@ -1,30 +1,16 @@
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
|
||||
export default function logout() {
|
||||
export default function logout({ redirect } = {}) {
|
||||
const ctx = helperContext();
|
||||
let siteSettings = ctx.siteSettings;
|
||||
let keyValueStore = ctx.keyValueStore;
|
||||
keyValueStore.abandonLocal();
|
||||
|
||||
const redirect = siteSettings.logout_redirect;
|
||||
if (!isEmpty(redirect)) {
|
||||
window.location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
const sso = siteSettings.enable_sso;
|
||||
const oneAuthenticator =
|
||||
!siteSettings.enable_local_logins && findAll().length === 1;
|
||||
|
||||
if (siteSettings.login_required && (sso || oneAuthenticator)) {
|
||||
// In this situation visiting most URLs will start the auth process again
|
||||
// Go to the `/login` page to avoid an immediate redirect
|
||||
window.location.href = getURL("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = getURL("/");
|
||||
}
|
||||
|
||||
@ -7,7 +7,14 @@ export function minimumOffset() {
|
||||
iPadNav = document.querySelector(".footer-nav-ipad .footer-nav"),
|
||||
iPadNavHeight = iPadNav ? iPadNav.offsetHeight : 0;
|
||||
|
||||
return header ? header.offsetHeight + iPadNavHeight : 0;
|
||||
// if the header has a positive offset from the top of the window, we need to include the offset
|
||||
// this covers cases where a site has a custom header above d-header (covers fixed and unfixed)
|
||||
const headerWrap = document.querySelector(".d-header-wrap"),
|
||||
headerWrapOffset = headerWrap.getBoundingClientRect();
|
||||
|
||||
return header
|
||||
? header.offsetHeight + headerWrapOffset.top + iPadNavHeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
export default function offsetCalculator() {
|
||||
|
||||
@ -654,6 +654,7 @@ function trimUnwanted(html) {
|
||||
const body = html.match(/<body[^>]*>([\s\S]*?)<\/body>/);
|
||||
html = body ? body[1] : html;
|
||||
html = html.replace(/\r|\n| /g, " ");
|
||||
html = html.replace(/\u00A0/g, " "); // trim no-break space
|
||||
|
||||
let match;
|
||||
while ((match = html.match(/<[^\s>]+[^>]*>\s{2,}<[^\s>]+[^>]*>/))) {
|
||||
|
||||
@ -140,12 +140,10 @@ export default function transformPost(
|
||||
postAtts.topicUrl = topic.get("url");
|
||||
postAtts.isSaving = post.isSaving;
|
||||
|
||||
if (post.notice_type) {
|
||||
postAtts.noticeType = post.notice_type;
|
||||
if (postAtts.noticeType === "custom") {
|
||||
postAtts.noticeMessage = post.notice_args;
|
||||
} else if (postAtts.noticeType === "returning_user") {
|
||||
postAtts.noticeTime = new Date(post.notice_args);
|
||||
if (post.notice) {
|
||||
postAtts.notice = post.notice;
|
||||
if (postAtts.notice.type === "returning_user") {
|
||||
postAtts.notice.lastPostedAt = new Date(post.notice.last_posted_at);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
app/assets/javascripts/discourse/app/lib/update-tab-count.js
Normal file
@ -0,0 +1,67 @@
|
||||
// This file's code is based on Favcount by Chris Hunt, Copyright 2013 Chris Hunt, MIT License
|
||||
|
||||
function renderIcon(canvas, img, count) {
|
||||
count = Math.round(count);
|
||||
if (isNaN(count) || count < 1) {
|
||||
count = "";
|
||||
} else if (count < 10) {
|
||||
count = " " + count;
|
||||
} else if (count > 99) {
|
||||
count = "99";
|
||||
}
|
||||
|
||||
// Scale canvas elements based on favicon size
|
||||
let multiplier = img.width / 16;
|
||||
let fontSize = multiplier * 11;
|
||||
let xOffset = multiplier;
|
||||
let shadow = multiplier * 2;
|
||||
|
||||
canvas.height = canvas.width = img.width;
|
||||
let ctx = canvas.getContext("2d");
|
||||
ctx.font = `bold ${fontSize}px Helvetica, Arial, sans-serif`;
|
||||
|
||||
if (count) {
|
||||
ctx.globalAlpha = 0.4;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
|
||||
// Draw white drop shadow
|
||||
ctx.shadowColor = "#FFF";
|
||||
ctx.shadowBlur = shadow;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
|
||||
// Draw white border
|
||||
ctx.fillStyle = "#FFF";
|
||||
ctx.fillText(count, xOffset, fontSize);
|
||||
ctx.fillText(count, xOffset + multiplier, fontSize);
|
||||
ctx.fillText(count, xOffset, fontSize + multiplier);
|
||||
ctx.fillText(count, xOffset + multiplier, fontSize + multiplier);
|
||||
|
||||
// Draw black count
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillText(count, xOffset + multiplier / 2.0, fontSize + multiplier / 2.0);
|
||||
|
||||
// Replace favicon with new favicon
|
||||
let newFavicon = document.createElement("link");
|
||||
newFavicon.rel = "icon";
|
||||
newFavicon.href = canvas.toDataURL("image/png");
|
||||
let favicon = document.querySelector("link[rel=icon]");
|
||||
|
||||
let head = document.querySelector("head");
|
||||
if (favicon) {
|
||||
head.removeChild(favicon);
|
||||
}
|
||||
head.appendChild(newFavicon);
|
||||
}
|
||||
|
||||
export default function tabCount(url, count) {
|
||||
let canvas = document.createElement("canvas");
|
||||
if (canvas.getContext) {
|
||||
let img = document.createElement("img");
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => renderIcon(canvas, img, count);
|
||||
img.src = url;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import afterTransition from "discourse/lib/after-transition";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import headerOutletHeights from "discourse/lib/header-outlet-height";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Mixin.create({
|
||||
@ -211,6 +212,10 @@ export default Mixin.create({
|
||||
}
|
||||
}
|
||||
|
||||
position.top -= this._calculateTopOffset(
|
||||
$("#main-outlet").offset(),
|
||||
headerOutletHeights()
|
||||
);
|
||||
if (isFixed) {
|
||||
position.top -= $("html").scrollTop();
|
||||
//if content is fixed and will be cut off on the bottom, display it above...
|
||||
@ -259,6 +264,13 @@ export default Mixin.create({
|
||||
});
|
||||
},
|
||||
|
||||
// some plugins/themes modify the page layout and may
|
||||
// need to override this calculation for the card to
|
||||
// position correctly
|
||||
_calculateTopOffset(mainOutletOffset, outletHeights) {
|
||||
return mainOutletOffset.top - outletHeights;
|
||||
},
|
||||
|
||||
_hide() {
|
||||
if (!this.visible) {
|
||||
$(this.element).css({ left: -9999, top: -9999 });
|
||||
|
||||
@ -43,11 +43,7 @@ CategoryList.reopenClass({
|
||||
}
|
||||
|
||||
if (c.topics) {
|
||||
c.topics = c.topics.map((t) => {
|
||||
const topic = Topic.create(t);
|
||||
topic.set("category", c);
|
||||
return topic;
|
||||
});
|
||||
c.topics = c.topics.map((t) => Topic.create(t));
|
||||
}
|
||||
|
||||
switch (statPeriod) {
|
||||
|
||||
@ -1127,26 +1127,22 @@ const Composer = RestModel.extend({
|
||||
|
||||
if (this.canEditTitle) {
|
||||
// Save title and/or post body
|
||||
if (!this.title && !this.reply) {
|
||||
if (isEmpty(this.title) && isEmpty(this.reply)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (
|
||||
this.title &&
|
||||
this.titleLengthValid &&
|
||||
this.reply &&
|
||||
this.replyLength < this.siteSettings.min_post_length
|
||||
) {
|
||||
// Do not save when both title and reply's length are too small
|
||||
if (!this.titleLengthValid && this.replyLength < this.minimumPostLength) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
} else {
|
||||
// Do not save when there is no reply
|
||||
if (!this.reply) {
|
||||
if (isEmpty(this.reply)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Do not save when the reply's length is too small
|
||||
if (this.replyLength < this.siteSettings.min_post_length) {
|
||||
if (this.replyLength < this.minimumPostLength) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ const NavItem = EmberObject.extend({
|
||||
}, this);
|
||||
|
||||
if (customHref) {
|
||||
return customHref;
|
||||
return getURL(customHref);
|
||||
}
|
||||
|
||||
const context = { category, noSubcategories, tagId };
|
||||
@ -122,7 +122,12 @@ NavItem.reopenClass({
|
||||
|
||||
if (context.tagId && Site.currentProp("filters").includes(filterType)) {
|
||||
includesTagContext = true;
|
||||
path += "/tags";
|
||||
|
||||
if (context.category) {
|
||||
path += "/tags";
|
||||
} else {
|
||||
path += "/tag";
|
||||
}
|
||||
}
|
||||
|
||||
if (context.category) {
|
||||
@ -245,6 +250,10 @@ NavItem.reopenClass({
|
||||
item.init(item, category, args);
|
||||
}
|
||||
|
||||
if (item.href) {
|
||||
item.href = getURL(item.href);
|
||||
}
|
||||
|
||||
const before = item.before;
|
||||
if (before) {
|
||||
let i = 0;
|
||||
|
||||
@ -737,6 +737,12 @@ export default RestModel.extend({
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
triggerDestroyedPost(postId) {
|
||||
const existing = this._identityMap[postId];
|
||||
this.removePosts([existing]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
triggerChangedPost(postId, updatedAt, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import { ajax } from "discourse/lib/ajax";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { Promise } from "rsvp";
|
||||
import Category from "discourse/models/category";
|
||||
import Session from "discourse/models/session";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import User from "discourse/models/user";
|
||||
@ -156,12 +155,10 @@ TopicList.reopenClass({
|
||||
|
||||
// Stitch together our side loaded data
|
||||
|
||||
const categories = Category.list(),
|
||||
users = extractByKey(result.users, User),
|
||||
groups = extractByKey(result.primary_groups, EmberObject);
|
||||
const users = extractByKey(result.users, User);
|
||||
const groups = extractByKey(result.primary_groups, EmberObject);
|
||||
|
||||
return result.topic_list[listKey].map((t) => {
|
||||
t.category = categories.findBy("id", t.category_id);
|
||||
t.posters.forEach((p) => {
|
||||
p.user = users[p.user_id];
|
||||
p.extraClasses = p.extras;
|
||||
|
||||
@ -6,6 +6,7 @@ import PreloadStore from "discourse/lib/preload-store";
|
||||
import Category from "discourse/models/category";
|
||||
import User from "discourse/models/user";
|
||||
import { deepEqual } from "discourse-common/lib/object";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
|
||||
function isNew(topic) {
|
||||
return (
|
||||
@ -148,6 +149,17 @@ const TopicTrackingState = EmberObject.extend({
|
||||
}
|
||||
tracker.incrementMessageCount();
|
||||
});
|
||||
|
||||
this.messageBus.subscribe("/destroy", (msg) => {
|
||||
tracker.incrementMessageCount();
|
||||
const currentRoute = DiscourseURL.router.currentRoute.parent;
|
||||
if (
|
||||
currentRoute.name === "topic" &&
|
||||
parseInt(currentRoute.params.id, 10) === msg.topic_id
|
||||
) {
|
||||
DiscourseURL.redirectTo("/");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mutedTopics() {
|
||||
|
||||
@ -13,10 +13,7 @@ import { emojiUnescape } from "discourse/lib/text";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { fancyTitle } from "discourse/lib/topic-fancy-title";
|
||||
import discourseComputed, {
|
||||
observes,
|
||||
on,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Category from "discourse/models/category";
|
||||
import Session from "discourse/models/session";
|
||||
import { Promise } from "rsvp";
|
||||
@ -227,10 +224,9 @@ const Topic = RestModel.extend({
|
||||
return { type: "topic", id };
|
||||
},
|
||||
|
||||
@on("init")
|
||||
@observes("category_id")
|
||||
_categoryIdChanged() {
|
||||
this.set("category", Category.findById(this.category_id));
|
||||
@discourseComputed("category_id")
|
||||
category(categoryId) {
|
||||
return Category.findById(categoryId);
|
||||
},
|
||||
|
||||
categoryClass: fmt("category.fullSlug", "category-%@"),
|
||||
|
||||
@ -13,7 +13,7 @@ import UserStream from "discourse/models/user-stream";
|
||||
import UserPostsStream from "discourse/models/user-posts-stream";
|
||||
import Singleton from "discourse/mixins/singleton";
|
||||
import { longDate } from "discourse/lib/formatter";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import Badge from "discourse/models/badge";
|
||||
import UserBadge from "discourse/models/user-badge";
|
||||
import UserActionStat from "discourse/models/user-action-stat";
|
||||
@ -729,41 +729,29 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
@observes("muted_category_ids")
|
||||
updateMutedCategories() {
|
||||
this.set("mutedCategories", Category.findByIds(this.muted_category_ids));
|
||||
@discourseComputed("muted_category_ids")
|
||||
mutedCategories(mutedCategoryIds) {
|
||||
return Category.findByIds(mutedCategoryIds);
|
||||
},
|
||||
|
||||
@observes("regular_category_ids")
|
||||
updateRegularCategories() {
|
||||
this.set(
|
||||
"regularCategories",
|
||||
Category.findByIds(this.regular_category_ids)
|
||||
);
|
||||
@discourseComputed("regular_category_ids")
|
||||
regularCategories(regularCategoryIds) {
|
||||
return Category.findByIds(regularCategoryIds);
|
||||
},
|
||||
|
||||
@observes("tracked_category_ids")
|
||||
updateTrackedCategories() {
|
||||
this.set(
|
||||
"trackedCategories",
|
||||
Category.findByIds(this.tracked_category_ids)
|
||||
);
|
||||
@discourseComputed("tracked_category_ids")
|
||||
trackedCategories(trackedCategoryIds) {
|
||||
return Category.findByIds(trackedCategoryIds);
|
||||
},
|
||||
|
||||
@observes("watched_category_ids")
|
||||
updateWatchedCategories() {
|
||||
this.set(
|
||||
"watchedCategories",
|
||||
Category.findByIds(this.watched_category_ids)
|
||||
);
|
||||
@discourseComputed("watched_category_ids")
|
||||
watchedCategories(watchedCategoryIds) {
|
||||
return Category.findByIds(watchedCategoryIds);
|
||||
},
|
||||
|
||||
@observes("watched_first_post_category_ids")
|
||||
updateWatchedFirstPostCategories() {
|
||||
this.set(
|
||||
"watchedFirstPostCategories",
|
||||
Category.findByIds(this.watched_first_post_category_ids)
|
||||
);
|
||||
@discourseComputed("watched_first_post_category_ids")
|
||||
watchedFirstPostCategories(wachedFirstPostCategoryIds) {
|
||||
return Category.findByIds(wachedFirstPostCategoryIds);
|
||||
},
|
||||
|
||||
@discourseComputed("can_delete_account")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import buildCategoryRoute from "discourse/routes/build-category-route";
|
||||
import buildTopicRoute from "discourse/routes/build-topic-route";
|
||||
import DiscoverySortableController from "discourse/controllers/discovery-sortable";
|
||||
import TagsShowRoute from "discourse/routes/tags-show";
|
||||
import TagShowRoute from "discourse/routes/tag-show";
|
||||
import Site from "discourse/models/site";
|
||||
import User from "discourse/models/user";
|
||||
|
||||
@ -11,20 +11,16 @@ export default {
|
||||
|
||||
initialize(registry, app) {
|
||||
app.DiscoveryCategoryController = DiscoverySortableController.extend();
|
||||
app.DiscoveryParentCategoryController = DiscoverySortableController.extend();
|
||||
app.DiscoveryCategoryNoneController = DiscoverySortableController.extend();
|
||||
app.DiscoveryCategoryAllController = DiscoverySortableController.extend();
|
||||
app.DiscoveryCategoryWithIDController = DiscoverySortableController.extend();
|
||||
|
||||
app.DiscoveryCategoryRoute = buildCategoryRoute("default");
|
||||
app.DiscoveryParentCategoryRoute = buildCategoryRoute("default");
|
||||
app.DiscoveryCategoryNoneRoute = buildCategoryRoute("default", {
|
||||
no_subcategories: true,
|
||||
});
|
||||
app.DiscoveryCategoryAllRoute = buildCategoryRoute("default", {
|
||||
no_subcategories: false,
|
||||
});
|
||||
app.DiscoveryCategoryWithIDRoute = buildCategoryRoute("default");
|
||||
|
||||
const site = Site.current();
|
||||
site.get("filters").forEach((filter) => {
|
||||
@ -35,9 +31,6 @@ export default {
|
||||
app[
|
||||
`Discovery${filterCapitalized}CategoryController`
|
||||
] = DiscoverySortableController.extend();
|
||||
app[
|
||||
`Discovery${filterCapitalized}ParentCategoryController`
|
||||
] = DiscoverySortableController.extend();
|
||||
app[
|
||||
`Discovery${filterCapitalized}CategoryNoneController`
|
||||
] = DiscoverySortableController.extend();
|
||||
@ -48,9 +41,6 @@ export default {
|
||||
app[`Discovery${filterCapitalized}CategoryRoute`] = buildCategoryRoute(
|
||||
filter
|
||||
);
|
||||
app[
|
||||
`Discovery${filterCapitalized}ParentCategoryRoute`
|
||||
] = buildCategoryRoute(filter);
|
||||
app[
|
||||
`Discovery${filterCapitalized}CategoryNoneRoute`
|
||||
] = buildCategoryRoute(filter, { no_subcategories: true });
|
||||
@ -74,9 +64,6 @@ export default {
|
||||
app[
|
||||
`DiscoveryTop${periodCapitalized}CategoryController`
|
||||
] = DiscoverySortableController.extend();
|
||||
app[
|
||||
`DiscoveryTop${periodCapitalized}ParentCategoryController`
|
||||
] = DiscoverySortableController.extend();
|
||||
app[
|
||||
`DiscoveryTop${periodCapitalized}CategoryNoneController`
|
||||
] = DiscoverySortableController.extend();
|
||||
@ -86,38 +73,26 @@ export default {
|
||||
app[`DiscoveryTop${periodCapitalized}CategoryRoute`] = buildCategoryRoute(
|
||||
"top/" + period
|
||||
);
|
||||
app[
|
||||
`DiscoveryTop${periodCapitalized}ParentCategoryRoute`
|
||||
] = buildCategoryRoute("top/" + period);
|
||||
app[
|
||||
`DiscoveryTop${periodCapitalized}CategoryNoneRoute`
|
||||
] = buildCategoryRoute("top/" + period, { no_subcategories: true });
|
||||
});
|
||||
|
||||
app["TagsShowCategoryRoute"] = TagsShowRoute.extend();
|
||||
app["TagsShowCategoryNoneRoute"] = TagsShowRoute.extend({
|
||||
app["TagsShowCategoryRoute"] = TagShowRoute.extend();
|
||||
app["TagsShowCategoryNoneRoute"] = TagShowRoute.extend({
|
||||
noSubcategories: true,
|
||||
});
|
||||
app["TagsShowParentCategoryRoute"] = TagsShowRoute.extend();
|
||||
|
||||
app["TagShowRoute"] = TagsShowRoute;
|
||||
|
||||
site.get("filters").forEach(function (filter) {
|
||||
app["TagsShow" + filter.capitalize() + "Route"] = TagsShowRoute.extend({
|
||||
navMode: filter,
|
||||
});
|
||||
app["TagShow" + filter.capitalize() + "Route"] = TagsShowRoute.extend({
|
||||
app["TagShow" + filter.capitalize() + "Route"] = TagShowRoute.extend({
|
||||
navMode: filter,
|
||||
});
|
||||
app[
|
||||
"TagsShowCategory" + filter.capitalize() + "Route"
|
||||
] = TagsShowRoute.extend({ navMode: filter });
|
||||
] = TagShowRoute.extend({ navMode: filter });
|
||||
app[
|
||||
"TagsShowNoneCategory" + filter.capitalize() + "Route"
|
||||
] = TagsShowRoute.extend({ navMode: filter, noSubcategories: true });
|
||||
app[
|
||||
"TagsShowParentCategory" + filter.capitalize() + "Route"
|
||||
] = TagsShowRoute.extend({ navMode: filter });
|
||||
] = TagShowRoute.extend({ navMode: filter, noSubcategories: true });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||