Version bump

This commit is contained in:
Neil Lalonde 2019-03-01 12:22:42 -05:00
commit a5df8c8dcf
1488 changed files with 12996 additions and 11044 deletions

View File

@ -6,9 +6,6 @@ app/assets/javascripts/ember-addons/
app/assets/javascripts/discourse/lib/autosize.js.es6
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/javascripts/moment.js
lib/javascripts/moment-timezone-with-data.js
lib/javascripts/moment_locale/
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/javascripts/

2
.rspec
View File

@ -1 +1,3 @@
--colour
--profile
--fail-fast

View File

@ -22,7 +22,10 @@ files = (git.added_files + git.modified_files)
.select { |path| !path.start_with?("plugins/") }
.select { |path| path.end_with?("es6") || path.end_with?("rb") }
js_test_files = files.select { |path| path.end_with?("-test.js.es6") }
super_offenses = []
jquery_find_offenses = []
files.each do |path|
diff = git.diff_for_file(path)
@ -34,9 +37,26 @@ files.each do |path|
end
end
js_test_files.each do |path|
diff = git.diff_for_file(path)
next if !diff
diff.patch.lines.grep(/^\+\s\s/).each do |added_line|
jquery_find_offenses << path if added_line['this.$(']
end
end
if !super_offenses.empty?
warn(%{
When possible use `this._super(...arguments)` instead of `this._super()`\n
#{super_offenses.uniq.map { |o| github.html_link(o) }.join("\n")}
})
end
if !jquery_find_offenses.empty?
warn(%{
Use `find()` instead of `this.$` in js tests`\n
#{jquery_find_offenses.uniq.map { |o| github.html_link(o) }.join("\n")}
})
end

View File

@ -44,7 +44,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.77'
gem 'onebox', '1.8.79'
gem 'http_accept_language', '~>2.0.5', require: false
@ -65,6 +65,7 @@ gem 'fast_xor', platform: :mri
gem 'fastimage'
gem 'aws-sdk-s3', require: false
gem 'aws-sdk-sns', require: false
gem 'excon', require: false
gem 'unf', require: false
@ -201,10 +202,11 @@ gem 'rchardet', require: false
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
gem 'sqlite3', '~> 1.3.13'
gem 'sqlite3', '~> 1.3', '>= 1.3.13'
gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md'
gem 'reverse_markdown'
gem 'tiny_tds'
gem 'csv', '~> 3.0'
end
gem 'webpush', require: false

View File

@ -44,19 +44,22 @@ GEM
arel (9.0.0)
ast (2.4.0)
aws-eventstream (1.0.1)
aws-partitions (1.104.0)
aws-sdk-core (3.27.0)
aws-partitions (1.138.0)
aws-sdk-core (3.46.1)
aws-eventstream (~> 1.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-kms (1.9.0)
aws-sdk-core (~> 3, >= 3.26.0)
aws-sdk-kms (1.13.0)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.19.0)
aws-sdk-core (~> 3, >= 3.26.0)
aws-sdk-s3 (1.30.1)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sdk-sns (1.9.0)
aws-sdk-core (~> 3, >= 3.39.0)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.3)
barber (0.12.0)
ember-source (>= 1.0, < 3.1)
@ -183,7 +186,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.0.1)
logster (2.1.2)
loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -202,7 +205,7 @@ GEM
libv8 (>= 6.3)
mini_scheduler (0.9.1)
sidekiq
mini_sql (0.1.10)
mini_sql (0.2.1)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.11.3)
@ -214,7 +217,7 @@ GEM
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustache (1.0.5)
mustache (1.1.0)
nap (1.1.0)
no_proxy_fix (0.1.2)
nokogiri (1.10.1)
@ -258,7 +261,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.77)
onebox (1.8.79)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@ -452,6 +455,7 @@ DEPENDENCIES
activesupport (= 5.2.2)
annotate
aws-sdk-s3
aws-sdk-sns
barber
better_errors
binding_of_caller
@ -511,7 +515,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.77)
onebox (= 1.8.79)
openid-redis-store
pg
pry-nav

View File

@ -1,4 +1,4 @@
<a href="http://www.discourse.org/"><img src=
<a href="https://www.discourse.org/"><img src=
"https://user-images.githubusercontent.com/1681963/52239617-e2683480-289c-11e9-922b-5da55472e5b4.png"
width="300px"></a>
@ -10,14 +10,14 @@ Discourse is the 100% open source discussion platform built for the next decade
- discussion forum
- long-form chat room
To learn more about the philosophy and goals of the project, [visit **discourse.org**](http://www.discourse.org).
To learn more about the philosophy and goals of the project, [visit **discourse.org**](https://www.discourse.org).
## Screenshots
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://user-images.githubusercontent.com/1681963/52239245-04ad8280-289c-11e9-9c88-8c173d4a0422.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
<a href="http://discuss.howtogeek.com"><img src="https://user-images.githubusercontent.com/1681963/52239247-04ad8280-289c-11e9-9706-fd66bc0749dc.png" width="720px"></a>
<a href="https://discuss.howtogeek.com"><img src="https://user-images.githubusercontent.com/1681963/52239247-04ad8280-289c-11e9-9706-fd66bc0749dc.png" width="720px"></a>
<a href="https://talk.turtlerockstudios.com/"><img src="https://user-images.githubusercontent.com/1681963/52239249-04ad8280-289c-11e9-9155-f0ccc5decc50.png" width="720px"></a>
<img src="https://user-images.githubusercontent.com/1681963/52239118-b304f800-289b-11e9-9904-16450680d9ec.jpg" alt="Mobile" width="414">
@ -34,7 +34,7 @@ To get your environment setup, follow the community setup guide for your operati
If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments.
Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](http://www.postgresql.org/download/), [Redis 2.6+](http://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 2.6+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
## Setting up Discourse
@ -57,8 +57,8 @@ Discourse is built for the *next* 10 years of the Internet, so our requirements
- [Ruby on Rails](https://github.com/rails/rails) &mdash; Our back end API is a Rails app. It responds to requests RESTfully in JSON.
- [Ember.js](https://github.com/emberjs/ember.js) &mdash; Our front end is an Ember.js app that communicates with the Rails API.
- [PostgreSQL](http://www.postgresql.org/) &mdash; Our main data store is in Postgres.
- [Redis](http://redis.io/) &mdash; We use Redis as a cache and for transient data.
- [PostgreSQL](https://www.postgresql.org/) &mdash; Our main data store is in Postgres.
- [Redis](https://redis.io/) &mdash; We use Redis as a cache and for transient data.
Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile).
@ -71,11 +71,11 @@ accepts contributions from the public &ndash; including you!
Before contributing to Discourse:
1. Please read the complete mission statements on [**discourse.org**](http://www.discourse.org). Yes we actually believe this stuff; you should too.
2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](http://discourse.org/cla).
1. Please read the complete mission statements on [**discourse.org**](https://www.discourse.org). Yes we actually believe this stuff; you should too.
2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](https://www.discourse.org/cla).
3. Dig into [**CONTRIBUTING.MD**](CONTRIBUTING.md), which covers submitting bugs, requesting new features, preparing your code for a pull request, etc.
4. Always strive to collaborate [with mutual respect](https://github.com/discourse/discourse/blob/master/docs/code-of-conduct.md).
5. Not sure what to work on? [**We've got some ideas.**](http://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823)
5. Not sure what to work on? [**We've got some ideas.**](https://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823)
We look forward to seeing your pull requests!
@ -86,7 +86,7 @@ We take security very seriously at Discourse; all our code is 100% open source a
## The Discourse Team
The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](http://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).
The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](https://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).
## Copyright / License
@ -97,7 +97,7 @@ Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@ -109,4 +109,4 @@ Discourse logo and “Discourse Forum” ®, Civilized Discourse Construction Ki
## Dedication
Discourse is built with [love, Internet style.](http://www.youtube.com/watch?v=Xe1TZaElTAs)
Discourse is built with [love, Internet style.](https://www.youtube.com/watch?v=Xe1TZaElTAs)

View File

@ -0,0 +1,91 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { fmt } from "discourse/lib/computed";
export default Ember.Component.extend({
@computed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) {
return targets.filter(target => {
if (target.advanced && !showAdvanced) {
return false;
}
if (!onlyOverridden) {
return true;
}
return target.edited;
});
},
@computed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) {
fields = fields[targetName];
if (onlyOverridden) {
fields = fields.filter(field => field.edited);
}
return fields;
},
@computed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) return "yaml";
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@computed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
}
},
editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
@computed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},
@computed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) {
return targets.find(t => t.name === currentTargetName).customNames;
},
@computed("currentTargetName", "fieldName", "theme.theme_fields.@each.error")
error(target, fieldName) {
return this.get("theme").getError(target, fieldName);
},
actions: {
toggleShowAdvanced() {
this.toggleProperty("showAdvanced");
},
toggleAddField() {
this.toggleProperty("addingField");
},
cancelAddField() {
this.set("addingField", false);
},
addField(name) {
if (!name) return;
name = name.replace(/\W/g, "");
this.get("theme").setField(this.get("currentTargetName"), name, "");
this.setProperties({ newFieldName: "", addingField: false });
this.fieldAdded(this.get("currentTargetName"), name);
},
toggleMaximize: function() {
this.toggleProperty("maximized");
Ember.run.next(() => this.appEvents.trigger("ace:resize"));
},
onlyOverriddenChanged(value) {
this.onlyOverriddenChanged(value);
}
}
});

View File

@ -2,6 +2,12 @@ import UserField from "admin/models/user-field";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyEqual } from "discourse/lib/computed";
import { i18n } from "discourse/lib/computed";
import {
default as computed,
observes,
on
} from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend(bufferedProperty("userField"), {
editing: Ember.computed.empty("userField.id"),
@ -10,58 +16,56 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
cantMoveUp: propertyEqual("userField", "firstField"),
cantMoveDown: propertyEqual("userField", "lastField"),
userFieldsDescription: function() {
return I18n.t("admin.user_fields.description");
}.property(),
userFieldsDescription: i18n("admin.user_fields.description"),
bufferedFieldType: function() {
return UserField.fieldTypeById(this.get("buffered.field_type"));
}.property("buffered.field_type"),
@computed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},
_focusOnEdit: function() {
@on("didInsertElement")
@observes("editing")
_focusOnEdit() {
if (this.get("editing")) {
Ember.run.scheduleOnce("afterRender", this, "_focusName");
}
}
.observes("editing")
.on("didInsertElement"),
},
_focusName: function() {
_focusName() {
$(".user-field-name").select();
},
fieldName: function() {
return UserField.fieldTypeById(this.get("userField.field_type")).get(
"name"
);
}.property("userField.field_type"),
@computed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},
flags: function() {
const ret = [];
if (this.get("userField.editable")) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (this.get("userField.required")) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (this.get("userField.show_on_profile")) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (this.get("userField.show_on_user_card")) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
return ret.join(", ");
}.property(
@computed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
),
)
flags(editable, required, showOnProfile, showOnUserCard) {
const ret = [];
if (editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
return ret.join(", ");
},
actions: {
save() {
const self = this;
const buffered = this.get("buffered");
const attrs = buffered.getProperties(
"name",
@ -76,9 +80,9 @@ export default Ember.Component.extend(bufferedProperty("userField"), {
this.get("userField")
.save(attrs)
.then(function() {
self.set("editing", false);
self.commitBuffer();
.then(() => {
this.set("editing", false);
this.commitBuffer();
})
.catch(popupAjaxError);
},

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ["install-theme-item"]
});

View File

@ -61,18 +61,6 @@ export default Ember.Component.extend({
}
},
didRender() {
this._super(...arguments);
// hide scrollbar
const $container = this.$(".themes-list-container");
const containerNode = $container[0];
if (containerNode) {
const width = containerNode.offsetWidth - containerNode.clientWidth;
$container.css("width", `calc(100% + ${width}px)`);
}
},
actions: {
changeView(newTab) {
if (newTab !== this.get("currentTab")) {

View File

@ -1,21 +1,25 @@
import showModal from "discourse/lib/show-modal";
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
baseColorScheme: function() {
@computed("model.@each.id")
baseColorScheme() {
return this.get("model").findBy("is_base", true);
}.property("model.@each.id"),
},
baseColorSchemes: function() {
@computed("model.@each.id")
baseColorSchemes() {
return this.get("model").filterBy("is_base", true);
}.property("model.@each.id"),
},
baseColors: function() {
var baseColorsHash = Ember.Object.create({});
this.get("baseColorScheme.colors").forEach(color => {
@computed("baseColorScheme")
baseColors(baseColorScheme) {
const baseColorsHash = Ember.Object.create({});
baseColorScheme.get("colors").forEach(color => {
baseColorsHash.set(color.get("name"), color);
});
return baseColorsHash;
}.property("baseColorScheme"),
},
actions: {
newColorSchemeWithBase(baseKey) {
@ -24,8 +28,10 @@ export default Ember.Controller.extend({
baseKey
);
const newColorScheme = Ember.copy(base, true);
newColorScheme.set("name", I18n.t("admin.customize.colors.new_name"));
newColorScheme.set("base_scheme_id", base.get("base_scheme_id"));
newColorScheme.setProperties({
name: I18n.t("admin.customize.colors.new_name"),
base_scheme_id: base.get("base_scheme_id")
});
newColorScheme.save().then(() => {
this.get("model").pushObject(newColorScheme);
newColorScheme.set("savingStatus", null);

View File

@ -1,163 +1,28 @@
import { url } from "discourse/lib/computed";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
section: null,
currentTarget: 0,
maximized: false,
previewUrl: url("model.id", "/admin/themes/%@/preview"),
showAdvanced: false,
editRouteName: "adminCustomizeThemes.edit",
targets: [
{ id: 0, name: "common" },
{ id: 1, name: "desktop" },
{ id: 2, name: "mobile" },
{ id: 3, name: "settings" },
{ id: 4, name: "translations" }
],
fieldsForTarget: function(target) {
const common = [
"scss",
"head_tag",
"header",
"after_header",
"body_tag",
"footer"
];
switch (target) {
case "common":
return [...common, "embedded_scss"];
case "desktop":
return common;
case "mobile":
return common;
case "settings":
return ["yaml"];
}
},
@computed("onlyOverridden")
showCommon() {
return this.shouldShow("common");
},
@computed("onlyOverridden")
showDesktop() {
return this.shouldShow("desktop");
},
@computed("onlyOverridden")
showMobile() {
return this.shouldShow("mobile");
},
@observes("onlyOverridden")
onlyOverriddenChanged() {
if (this.get("onlyOverridden")) {
if (
!this.get("model").hasEdited(
this.get("currentTargetName"),
this.get("fieldName")
)
) {
let target =
(this.get("showCommon") && "common") ||
(this.get("showDesktop") && "desktop") ||
(this.get("showMobile") && "mobile");
let fields = this.get("model.theme_fields");
let field = fields && fields.find(f => f.target === target);
this.replaceRoute(
this.get("editRouteName"),
this.get("model.id"),
target,
field && field.name
);
}
}
},
shouldShow(target) {
if (!this.get("onlyOverridden")) {
return true;
}
return this.get("model").hasEdited(target);
},
showRouteName: "adminCustomizeThemes.show",
setTargetName: function(name) {
const target = this.get("targets").find(t => t.name === name);
const target = this.get("model.targets").find(t => t.name === name);
this.set("currentTarget", target && target.id);
},
@computed("currentTarget")
currentTargetName(id) {
const target = this.get("targets").find(t => t.id === parseInt(id, 10));
const target = this.get("model.targets").find(
t => t.id === parseInt(id, 10)
);
return target && target.name;
},
@computed("fieldName")
activeSectionMode(fieldName) {
if (fieldName === "yaml") return "yaml";
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@computed("currentTargetName", "fieldName", "saving")
error(target, fieldName) {
return this.get("model").getError(target, fieldName);
},
@computed("fieldName", "currentTargetName")
editorId(fieldName, currentTarget) {
return fieldName + "|" + currentTarget;
},
@computed("fieldName", "currentTargetName", "model")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
}
},
@computed("currentTargetName", "onlyOverridden")
fields(target, onlyOverridden) {
let fields = this.fieldsForTarget(target);
if (onlyOverridden) {
const model = this.get("model");
const targetName = this.get("currentTargetName");
fields = fields.filter(name => model.hasEdited(targetName, name));
}
return fields.map(name => {
let hash = {
key: `admin.customize.theme.${name}.text`,
name: name
};
if (name.indexOf("_tag") > 0) {
hash.icon = "file-text-o";
}
hash.title = I18n.t(`admin.customize.theme.${name}.title`);
return hash;
});
},
@computed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},
@computed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
@ -178,11 +43,36 @@ export default Ember.Controller.extend({
});
},
toggleMaximize: function() {
this.toggleProperty("maximized");
Ember.run.next(() => {
this.appEvents.trigger("ace:resize");
});
fieldAdded(target, name) {
this.replaceRoute(
this.get("editRouteName"),
this.get("model.id"),
target,
name
);
},
onlyOverriddenChanged(onlyShowOverridden) {
if (onlyShowOverridden) {
if (
!this.get("model").hasEdited(
this.get("currentTargetName"),
this.get("fieldName")
)
) {
let firstTarget = this.get("model.targets").find(t => t.edited);
let firstField = this.get(`model.fields.${firstTarget.name}`).find(
f => f.edited
);
this.replaceRoute(
this.get("editRouteName"),
this.get("model.id"),
firstTarget.name,
firstField.name
);
}
}
}
}
});

View File

@ -147,6 +147,10 @@ export default Ember.Controller.extend({
"scss"
);
},
sourceIsHttp: Ember.computed.match(
"model.remote_theme.remote_url",
/^http(s)?:\/\//
),
actions: {
updateToLatest() {
this.set("updatingRemote", true);

View File

@ -12,5 +12,10 @@ export default Ember.Controller.extend({
@computed("model", "model.@each.component")
childThemes(themes) {
return themes.filter(t => t.get("component"));
},
@computed("model", "model.@each.component")
installedThemes(themes) {
return themes.map(t => t.name);
}
});

View File

@ -1,9 +1,8 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import debounce from "discourse/lib/debounce";
import EmailLog from "admin/models/email-log";
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.{user,address,type}")
this.loadLogs();
}, 250).observes("filter.{status,user,address,type}")
});

View File

@ -1,25 +0,0 @@
import IncomingEmail from "admin/models/incoming-email";
export default Ember.Controller.extend({
loading: false,
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) {
return;
}
this.set("loading", true);
IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
.then(incoming => {
if (incoming.length < 50) {
this.get("model").set("allLoaded", true);
}
this.get("model").addObjects(incoming);
})
.finally(() => {
this.set("loading", false);
});
}
}
});

View File

@ -33,7 +33,7 @@ export default Ember.Controller.extend({
data: { email_address: this.get("testEmailAddress") }
})
.then(response =>
this.set("sentTestEmailMessage", response.send_test_email_message)
this.set("sentTestEmailMessage", response.sent_test_email_message)
)
.catch(e => {
if (e.responseJSON && e.responseJSON.errors) {

View File

@ -3,23 +3,34 @@ import EmailLog from "admin/models/email-log";
export default Ember.Controller.extend({
loading: false,
loadLogs(sourceModel, loadMore) {
if ((loadMore && this.get("loading")) || this.get("model.allLoaded")) {
return;
}
this.set("loading", true);
sourceModel = sourceModel || EmailLog;
return sourceModel
.findAll(this.get("filter"), loadMore ? this.get("model.length") : null)
.then(logs => {
if (this.get("model") && loadMore && logs.length < 50) {
this.get("model").set("allLoaded", true);
}
if (this.get("model") && loadMore) {
this.get("model").addObjects(logs);
} else {
this.set("model", logs);
}
})
.finally(() => this.set("loading", false));
},
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) {
return;
}
this.set("loading", true);
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
.then(logs => {
if (logs.length < 50) {
this.get("model").set("allLoaded", true);
}
this.get("model").addObjects(logs);
})
.finally(() => {
this.set("loading", false);
});
this.loadLogs(EmailLog, true);
}
}
});

View File

@ -1,11 +1,15 @@
import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings";
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import debounce from "discourse/lib/debounce";
import IncomingEmail from "admin/models/incoming-email";
export default AdminEmailIncomingsController.extend({
export default AdminEmailLogsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings =>
this.set("model", incomings)
);
}, 250).observes("filter.{from,to,subject}")
this.loadLogs(IncomingEmail);
}, 250).observes("filter.{status,from,to,subject}"),
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
}
}
});

View File

@ -1,11 +1,15 @@
import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings";
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import debounce from "discourse/lib/debounce";
import IncomingEmail from "admin/models/incoming-email";
export default AdminEmailIncomingsController.extend({
export default AdminEmailLogsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings =>
this.set("model", incomings)
);
}, 250).observes("filter.{from,to,subject,error}")
this.loadLogs(IncomingEmail);
}, 250).observes("filter.{status,from,to,subject,error}"),
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
}
}
});

View File

@ -1,9 +1,8 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import debounce from "discourse/lib/debounce";
import EmailLog from "admin/models/email-log";
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.{user,address,type,reply_key}")
this.loadLogs();
}, 250).observes("filter.{status,user,address,type,reply_key}")
});

View File

@ -1,9 +1,8 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import debounce from "discourse/lib/debounce";
import EmailLog from "admin/models/email-log";
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.{user,address,type}")
this.loadLogs();
}, 250).observes("filter.{status,user,address,type}")
});

View File

@ -1,5 +1,4 @@
let lastSearch;
let lastOverridden;
export default Ember.Controller.extend({
searching: false,
@ -8,7 +7,7 @@ export default Ember.Controller.extend({
queryParams: ["q", "overridden"],
q: null,
overridden: null,
overridden: false,
_performSearch() {
this.store
@ -24,14 +23,18 @@ export default Ember.Controller.extend({
this.transitionToRoute("adminSiteText.edit", siteText.get("id"));
},
search(overridden) {
if (typeof overridden === "boolean") this.set("overridden", overridden);
toggleOverridden() {
this.toggleProperty("overridden");
this.set("searching", true);
Ember.run.debounce(this, this._performSearch, 400);
},
search() {
const q = this.get("q");
if (q !== lastSearch || overridden !== lastOverridden) {
if (q !== lastSearch) {
this.set("searching", true);
Ember.run.debounce(this, this._performSearch, 400);
lastSearch = q;
lastOverridden = overridden;
}
}
}

View File

@ -48,7 +48,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
return automaticGroups
.map(group => {
const name = Ember.String.htmlSafe(group.name);
return `<a href="/groups/${name}">${name}</a>`;
return `<a href="/g/${name}">${name}</a>`;
})
.join(", ");
},
@ -143,9 +143,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
resetBounceScore() {
return this.get("model").resetBounceScore();
},
refreshBrowsers() {
return this.get("model").refreshBrowsers();
},
approve() {
return this.get("model").approve();
},

View File

@ -1,12 +1,11 @@
import debounce from "discourse/lib/debounce";
import { i18n } from "discourse/lib/computed";
import AdminUser from "admin/models/admin-user";
import { observes } from "ember-addons/ember-computed-decorators";
import CanCheckEmails from "discourse/mixins/can-check-emails";
export default Ember.Controller.extend(CanCheckEmails, {
model: null,
query: null,
queryParams: ["order", "ascending"],
order: null,
ascending: null,
showEmails: false,
@ -47,8 +46,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
this._refreshUsers();
}, 250).observes("listFilter"),
@observes("order", "ascending")
_refreshUsers: function() {
_refreshUsers() {
this.set("refreshing", true);
AdminUser.findAll(this.get("query"), {
@ -57,12 +55,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
order: this.get("order"),
ascending: this.get("ascending")
})
.then(result => {
this.set("model", result);
})
.finally(() => {
this.set("refreshing", false);
});
.then(result => this.set("model", result))
.finally(() => this.set("refreshing", false));
},
actions: {
@ -95,7 +89,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
showEmails: function() {
this.set("showEmails", true);
this._refreshUsers(true);
this._refreshUsers();
}
}
});

View File

@ -1,3 +1,4 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { escapeExpression } from "discourse/lib/utilities";
export default Ember.Controller.extend({
@ -5,43 +6,42 @@ export default Ember.Controller.extend({
errors: Ember.computed.alias("model.errors"),
count: Ember.computed.alias("model.grant_count"),
count_warning: function() {
if (this.get("count") <= 10) {
return this.get("sample.length") !== this.get("count");
@computed("count", "sample.length")
countWarning(count, sampleLength) {
if (count <= 10) {
return sampleLength !== count;
} else {
return this.get("sample.length") !== 10;
return sampleLength !== 10;
}
}.property("count", "sample.length"),
},
has_query_plan: function() {
return !!this.get("model.query_plan");
}.property("model.query_plan"),
@computed("model.query_plan")
hasQueryPlan(queryPlan) {
return !!queryPlan;
},
query_plan_html: function() {
var raw = this.get("model.query_plan"),
returned = "<pre class='badge-query-plan'>";
@computed("model.query_plan")
queryPlanHtml(queryPlan) {
let output = `<pre class="badge-query-plan">`;
raw.forEach(linehash => {
returned += escapeExpression(linehash["QUERY PLAN"]);
returned += "<br>";
queryPlan.forEach(linehash => {
output += escapeExpression(linehash["QUERY PLAN"]);
output += "<br>";
});
returned += "</pre>";
return returned;
}.property("model.query_plan"),
output += "</pre>";
return output;
},
processed_sample: Ember.computed.map("model.sample", function(grant) {
var i18nKey = "admin.badges.preview.grant.with",
i18nParams = { username: escapeExpression(grant.username) };
processedSample: Ember.computed.map("model.sample", grant => {
let i18nKey = "admin.badges.preview.grant.with";
const i18nParams = { username: escapeExpression(grant.username) };
if (grant.post_id) {
i18nKey += "_post";
i18nParams.link =
"<a href='/p/" +
grant.post_id +
"' data-auto-route='true'>" +
Handlebars.Utils.escapeExpression(grant.title) +
"</a>";
i18nParams.link = `<a href="/p/${grant.post_id}" data-auto-route="true">
${Handlebars.Utils.escapeExpression(grant.title)}
</a>`;
}
if (grant.granted_at) {

View File

@ -1,65 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { default as computed } from "ember-addons/ember-computed-decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { THEMES, COMPONENTS } from "admin/models/theme";
const MIN_NAME_LENGTH = 4;
export default Ember.Controller.extend(ModalFunctionality, {
saving: false,
triggerError: false,
themesController: Ember.inject.controller("adminCustomizeThemes"),
types: [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS }
],
@computed("triggerError", "nameTooShort")
showError(trigger, tooShort) {
return trigger && tooShort;
},
@computed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
},
@computed("component")
placeholder(component) {
if (component) {
return I18n.t("admin.customize.theme.component_name");
} else {
return I18n.t("admin.customize.theme.theme_name");
}
},
@computed("themesController.currentTab")
selectedType(tab) {
return tab;
},
@computed("selectedType")
component(type) {
return type === COMPONENTS;
},
actions: {
createTheme() {
if (this.get("nameTooShort")) {
this.set("triggerError", true);
return;
}
this.set("saving", true);
const theme = this.store.createRecord("theme");
theme
.save({ name: this.get("name"), component: this.get("component") })
.then(() => {
this.get("themesController").send("addTheme", theme);
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
}
}
});

View File

@ -1,22 +1,24 @@
import { ajax } from "discourse/lib/ajax";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { observes } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
modelChanged: function() {
@observes("model")
modelChanged() {
const model = this.get("model");
const copy = Ember.A();
const store = this.store;
if (model) {
model.forEach(function(o) {
copy.pushObject(store.createRecord("badge-grouping", o));
});
model.forEach(o =>
copy.pushObject(store.createRecord("badge-grouping", o))
);
}
this.set("workingCopy", copy);
}.observes("model"),
},
moveItem: function(item, delta) {
moveItem(item, delta) {
const copy = this.get("workingCopy");
const index = copy.indexOf(item);
if (index + delta < 0 || index + delta >= copy.length) {
@ -28,60 +30,51 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
actions: {
up: function(item) {
up(item) {
this.moveItem(item, -1);
},
down: function(item) {
down(item) {
this.moveItem(item, 1);
},
delete: function(item) {
delete(item) {
this.get("workingCopy").removeObject(item);
},
cancel: function() {
this.set("model", null);
this.set("workingCopy", null);
cancel() {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
edit: function(item) {
edit(item) {
item.set("editing", true);
},
save: function(item) {
save(item) {
item.set("editing", false);
},
add: function() {
add() {
const obj = this.store.createRecord("badge-grouping", {
editing: true,
name: I18n.t("admin.badges.badge_grouping")
});
this.get("workingCopy").pushObject(obj);
},
saveAll: function() {
const self = this;
var items = this.get("workingCopy");
const groupIds = items.map(function(i) {
return i.get("id") || -1;
});
const names = items.map(function(i) {
return i.get("name");
});
saveAll() {
let items = this.get("workingCopy");
const groupIds = items.map(i => i.get("id") || -1);
const names = items.map(i => i.get("name"));
ajax("/admin/badges/badge_groupings", {
data: { ids: groupIds, names: names },
data: { ids: groupIds, names },
method: "POST"
}).then(
function(data) {
items = self.get("model");
data => {
items = this.get("model");
items.clear();
data.badge_groupings.forEach(function(g) {
items.pushObject(self.store.createRecord("badge-grouping", g));
data.badge_groupings.forEach(g => {
items.pushObject(this.store.createRecord("badge-grouping", g));
});
self.set("model", null);
self.set("workingCopy", null);
self.send("closeModal");
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
function() {
bootbox.alert(I18n.t("generic_error"));
}
() => bootbox.alert(I18n.t("generic_error"))
);
}
}

View File

@ -1,89 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
local: Ember.computed.equal("selection", "local"),
remote: Ember.computed.equal("selection", "remote"),
selection: "local",
adminCustomizeThemes: Ember.inject.controller(),
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
checkPrivate: Ember.computed.match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
urlPlaceholder: "https://github.com/discourse/sample_theme",
@computed("loading", "remote", "uploadUrl", "local", "localFile")
importDisabled(isLoading, isRemote, uploadUrl, isLocal, localFile) {
return isLoading || (isRemote && !uploadUrl) || (isLocal && !localFile);
},
@observes("privateChecked")
privateWasChecked() {
this.get("privateChecked")
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
const checked = this.get("privateChecked");
if (checked && !this._keyLoading) {
this._keyLoading = true;
ajax(this.get("keyGenUrl"), { method: "POST" })
.then(pair => {
this.set("privateKey", pair.private_key);
this.set("publicKey", pair.public_key);
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
actions: {
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
},
importTheme() {
let options = {
type: "POST"
};
if (this.get("local")) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.get("localFile"));
} else {
options.data = {
remote: this.get("uploadUrl"),
branch: this.get("branch")
};
if (this.get("privateChecked")) {
options.data.private_key = this.get("privateKey");
}
}
this.set("loading", true);
ajax(this.get("importUrl"), options)
.then(result => {
const theme = this.store.createRecord("theme", result.theme);
this.get("adminCustomizeThemes").send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.set("privateKey", null);
this.set("publicKey", null);
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
}
}
});

View File

@ -0,0 +1,290 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import { THEMES, COMPONENTS } from "admin/models/theme";
const MIN_NAME_LENGTH = 4;
// TODO: use a central repository for themes/components
const POPULAR_THEMES = [
{
name: "Graceful",
value: "https://github.com/discourse/graceful",
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
description: "A light and graceful theme for Discourse.",
meta_url:
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040"
},
{
name: "Material Design Theme",
value: "https://github.com/discourse/material-design-stock-theme",
preview: "https://newmaterial.trydiscourse.com",
description:
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142"
},
{
name: "Minima",
value: "https://github.com/discourse/minima",
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
description: "A minimal theme with reduced UI elements and focus on text.",
meta_url:
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178"
},
{
name: "Sam's Simple Theme",
value: "https://github.com/discourse/discourse-simple-theme",
preview: "https://theme-creator.discourse.org/theme/sam/simple",
description:
"Simplified front page design with classic colors and typography.",
meta_url:
"https://meta.discourse.org/t/sams-personal-minimal-topic-list-design/23552"
},
{
name: "Vincent",
value: "https://github.com/discourse/discourse-vincent-theme",
preview: "https://theme-creator.discourse.org/theme/awesomerobot/vincent",
description: "An elegant dark theme with a few color palettes.",
meta_url: "https://meta.discourse.org/t/discourse-vincent-theme/76662"
},
{
name: "Alternative Logos",
value: "https://github.com/discourse/discourse-alt-logo",
description: "Add alternative logos for dark / light themes.",
meta_url:
"https://meta.discourse.org/t/alternative-logo-for-dark-themes/88502",
component: true
},
{
name: "Brand Header Theme Component",
value: "https://github.com/discourse/discourse-brand-header",
description:
"Add an extra top header with your logo, navigation links and social icons.",
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
component: true
},
{
name: "Custom Header Links",
value: "https://github.com/discourse/discourse-custom-header-links",
preview:
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
description: "Easily add custom text-based links to the header.",
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
component: true
},
{
name: "Category Banners",
value: "https://github.com/discourse/discourse-category-banners",
preview:
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
description:
"Show banners on category pages using your existing category details.",
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
component: true
},
{
name: "Hamburger Theme Selector",
value: "https://github.com/discourse/discourse-hamburger-theme-selector",
description:
"Displays a theme selector in the hamburger menu provided there is more than one user-selectable theme.",
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
component: true
},
{
name: "Header submenus",
value: "https://github.com/discourse/discourse-header-submenus",
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
description: "Lets you build a header menu with submenus (dropdowns).",
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
component: true
}
];
export default Ember.Controller.extend(ModalFunctionality, {
popular: Ember.computed.equal("selection", "popular"),
local: Ember.computed.equal("selection", "local"),
remote: Ember.computed.equal("selection", "remote"),
create: Ember.computed.equal("selection", "create"),
selection: "popular",
adminCustomizeThemes: Ember.inject.controller(),
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: Ember.computed.match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
urlPlaceholder: "https://github.com/discourse/sample_theme",
advancedVisible: false,
themesController: Ember.inject.controller("adminCustomizeThemes"),
createTypes: [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS }
],
selectedType: Ember.computed.alias("themesController.currentTab"),
component: Ember.computed.equal("selectedType", COMPONENTS),
@computed("themesController.installedThemes")
themes(installedThemes) {
return POPULAR_THEMES.map(t => {
if (installedThemes.includes(t.name)) {
Ember.set(t, "installed", true);
}
return t;
});
},
@computed(
"loading",
"remote",
"uploadUrl",
"local",
"localFile",
"create",
"nameTooShort"
)
installDisabled(
isLoading,
isRemote,
uploadUrl,
isLocal,
localFile,
isCreate,
nameTooShort
) {
return (
isLoading ||
(isRemote && !uploadUrl) ||
(isLocal && !localFile) ||
(isCreate && nameTooShort)
);
},
@observes("privateChecked")
privateWasChecked() {
this.get("privateChecked")
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
const checked = this.get("privateChecked");
if (checked && !this._keyLoading) {
this._keyLoading = true;
ajax(this.get("keyGenUrl"), { method: "POST" })
.then(pair => {
this.setProperties({
privateKey: pair.private_key,
publicKey: pair.public_key
});
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
@computed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
},
@computed("component")
placeholder(component) {
if (component) {
return I18n.t("admin.customize.theme.component_name");
} else {
return I18n.t("admin.customize.theme.theme_name");
}
},
@computed("selection")
submitLabel(selection) {
return `admin.customize.theme.${
selection === "create" ? "create" : "install"
}`;
},
@computed("privateChecked", "checkPrivate", "publicKey")
showPublicKey(privateChecked, checkPrivate, publicKey) {
return privateChecked && checkPrivate && publicKey;
},
actions: {
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
},
toggleAdvanced() {
this.toggleProperty("advancedVisible");
},
installThemeFromList(url) {
this.set("uploadUrl", url);
this.send("installTheme");
},
installTheme() {
if (this.get("create")) {
this.set("loading", true);
const theme = this.store.createRecord(this.get("recordType"));
theme
.save({ name: this.get("name"), component: this.get("component") })
.then(() => {
this.get("themesController").send("addTheme", theme);
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
return;
}
let options = {
type: "POST"
};
if (this.get("local")) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.get("localFile"));
}
if (this.get("remote") || this.get("popular")) {
options.data = {
remote: this.get("uploadUrl"),
branch: this.get("branch")
};
if (this.get("privateChecked")) {
options.data.private_key = this.get("privateKey");
}
}
if (this.get("model.user_id")) {
// Used by theme-creator
options.data["user_id"] = this.get("model.user_id");
}
this.set("loading", true);
ajax(this.get("importUrl"), options)
.then(result => {
const theme = this.store.createRecord(
this.get("recordType"),
result.theme
);
this.get("adminCustomizeThemes").send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.setProperties({ privateKey: null, publicKey: null });
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
}
}
});

View File

@ -226,14 +226,6 @@ const AdminUser = Discourse.User.extend({
.catch(popupAjaxError);
},
refreshBrowsers() {
return ajax("/admin/users/" + this.get("id") + "/refresh_browsers", {
type: "POST"
}).finally(() =>
bootbox.alert(I18n.t("admin.user.refresh_browsers_message"))
);
},
approve() {
const self = this;
return ajax("/admin/users/" + this.get("id") + "/approve", {

View File

@ -1,4 +1,5 @@
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
const Backup = Discourse.Model.extend({
destroy() {
@ -15,9 +16,16 @@ const Backup = Discourse.Model.extend({
Backup.reopenClass({
find() {
return ajax("/admin/backups.json").then(backups =>
backups.map(backup => Backup.create(backup))
);
return ajax("/admin/backups.json")
.then(backups => backups.map(backup => Backup.create(backup)))
.catch(error => {
bootbox.alert(
I18n.t("admin.backups.backup_storage_error", {
error_message: extractError(error)
})
);
return [];
});
},
start(withUploads) {

View File

@ -1,48 +1,50 @@
const ColorSchemeColor = Discourse.Model.extend({
init: function() {
this._super(...arguments);
this.startTrackingChanges();
},
import {
default as computed,
observes,
on
} from "ember-addons/ember-computed-decorators";
import { propertyNotEqual, i18n } from "discourse/lib/computed";
startTrackingChanges: function() {
const ColorSchemeColor = Discourse.Model.extend({
@on("init")
startTrackingChanges() {
this.set("originals", { hex: this.get("hex") || "FFFFFF" });
this.notifyPropertyChange("hex"); // force changed property to be recalculated
// force changed property to be recalculated
this.notifyPropertyChange("hex");
},
// Whether value has changed since it was last saved.
changed: function() {
if (!this.originals) return false;
if (this.get("hex") !== this.originals["hex"]) return true;
@computed("hex")
changed(hex) {
if (!this.get("originals")) return false;
if (hex !== this.get("originals").hex) return true;
return false;
}.property("hex"),
},
// Whether the current value is different than Discourse's default color scheme.
overridden: function() {
return this.get("hex") !== this.get("default_hex");
}.property("hex", "default_hex"),
overridden: propertyNotEqual("hex", "default_hex"),
// Whether the saved value is different than Discourse's default color scheme.
savedIsOverriden: function() {
return this.get("originals").hex !== this.get("default_hex");
}.property("hex", "default_hex"),
@computed("default_hex", "hex")
savedIsOverriden(defaultHex) {
return this.get("originals").hex !== defaultHex;
},
revert: function() {
revert() {
this.set("hex", this.get("default_hex"));
},
undo: function() {
if (this.originals) this.set("hex", this.originals["hex"]);
undo() {
if (this.get("originals")) {
this.set("hex", this.get("originals").hex);
}
},
translatedName: function() {
return I18n.t("admin.customize.colors." + this.get("name") + ".name");
}.property("name"),
translatedName: i18n("name", "admin.customize.colors.%@.name"),
description: function() {
return I18n.t(
"admin.customize.colors." + this.get("name") + ".description"
);
}.property("name"),
description: i18n("name", "admin.customize.colors.%@.description"),
/**
brightness returns a number between 0 (darkest) to 255 (brightest).
@ -50,8 +52,8 @@ const ColorSchemeColor = Discourse.Model.extend({
@property brightness
**/
brightness: function() {
var hex = this.get("hex");
@computed("hex")
brightness(hex) {
if (hex.length === 6 || hex.length === 3) {
if (hex.length === 3) {
hex =
@ -69,9 +71,10 @@ const ColorSchemeColor = Discourse.Model.extend({
1000
);
}
}.property("hex"),
},
hexValueChanged: function() {
@observes("hex")
hexValueChanged() {
if (this.get("hex")) {
this.set(
"hex",
@ -80,11 +83,12 @@ const ColorSchemeColor = Discourse.Model.extend({
.replace(/[^0-9a-fA-F]/g, "")
);
}
}.observes("hex"),
},
valid: function() {
return this.get("hex").match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
}.property("hex")
@computed("hex")
valid(hex) {
return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
}
});
export default ColorSchemeColor;

View File

@ -9,11 +9,87 @@ export const COMPONENTS = "components";
const SETTINGS_TYPE_ID = 5;
const Theme = RestModel.extend({
FIELDS_IDS: [0, 1],
FIELDS_IDS: [0, 1, 5],
isActive: Ember.computed.or("default", "user_selectable"),
isPendingUpdates: Ember.computed.gt("remote_theme.commits_behind", 0),
hasEditedFields: Ember.computed.gt("editedFields.length", 0),
@computed("theme_fields.[]")
targets() {
return [
{ id: 0, name: "common" },
{ id: 1, name: "desktop", icon: "desktop" },
{ id: 2, name: "mobile", icon: "mobile-alt" },
{ id: 3, name: "settings", icon: "cog", advanced: true },
{
id: 4,
name: "translations",
icon: "globe",
advanced: true,
customNames: true
}
].map(target => {
target["edited"] = this.hasEdited(target.name);
target["error"] = this.hasError(target.name);
return target;
});
},
@computed("theme_fields.[]")
fieldNames() {
const common = [
"scss",
"head_tag",
"header",
"after_header",
"body_tag",
"footer"
];
return {
common: [...common, "embedded_scss"],
desktop: common,
mobile: common,
settings: ["yaml"],
translations: [
"en",
...(this.get("theme_fields") || [])
.filter(f => f.target === "translations" && f.name !== "en")
.map(f => f.name)
]
};
},
@computed("fieldNames", "theme_fields.[]", "theme_fields.@each.error")
fields(fieldNames) {
const hash = {};
Object.keys(fieldNames).forEach(target => {
hash[target] = fieldNames[target].map(fieldName => {
const field = {
name: fieldName,
edited: this.hasEdited(target, fieldName),
error: this.hasError(target, fieldName)
};
if (target === "translations") {
field.translatedName = fieldName;
} else {
field.translatedName = I18n.t(
`admin.customize.theme.${fieldName}.text`
);
field.title = I18n.t(`admin.customize.theme.${fieldName}.title`);
}
if (fieldName.indexOf("_tag") > 0) {
field.icon = "far-file-alt";
}
return field;
});
});
return hash;
},
@computed("theme_fields")
themeFields(fields) {
if (!fields) {
@ -76,6 +152,12 @@ const Theme = RestModel.extend({
}
},
hasError(target, name) {
return this.get("theme_fields")
.filter(f => f.target === target && (!name || name === f.name))
.any(f => f.error);
},
getError(target, name) {
let themeFields = this.get("themeFields");
let key = this.getKey({ target, name });
@ -114,7 +196,7 @@ const Theme = RestModel.extend({
existing.value = value;
existing.upload_id = upload_id;
} else {
fields.push(field);
fields.pushObject(field);
}
return;
}
@ -123,10 +205,19 @@ const Theme = RestModel.extend({
let key = this.getKey({ target, name });
let existingField = themeFields[key];
if (!existingField) {
this.theme_fields.push(field);
this.theme_fields.pushObject(field);
themeFields[key] = field;
} else {
const changed =
(Ember.isEmpty(existingField.value) && !Ember.isEmpty(value)) ||
(Ember.isEmpty(value) && !Ember.isEmpty(existingField.value));
existingField.value = value;
if (changed) {
// Observing theme_fields.@each.value is too slow, so manually notify
// if the value goes to/from blank
this.notifyPropertyChange("theme_fields.[]");
}
}
},

View File

@ -21,7 +21,7 @@ export default Ember.Route.extend({
},
setupController(controller, wrapper) {
const fields = controller.fieldsForTarget(wrapper.target);
const fields = wrapper.model.get("fields")[wrapper.target].map(f => f.name);
if (!fields.includes(wrapper.field_name)) {
this.transitionTo(
"adminCustomizeThemes.edit",

View File

@ -11,17 +11,13 @@ export default Ember.Route.extend({
},
actions: {
importModal() {
showModal("admin-import-theme", { admin: true });
installModal() {
showModal("admin-install-theme", { admin: true });
},
addTheme(theme) {
this.refresh();
this.transitionTo("adminCustomizeThemes.show", theme.get("id"));
},
showCreateModal() {
showModal("admin-create-theme", { admin: true });
}
}
});

View File

@ -1,12 +1,8 @@
import EmailLog from "admin/models/email-log";
export default Discourse.Route.extend({
model() {
return EmailLog.findAll({ status: this.get("status") });
},
setupController(controller, model) {
controller.set("model", model);
controller.set("filter", { status: this.get("status") });
setupController(controller) {
controller.setProperties({
loading: true,
filter: { status: this.get("status") }
});
}
});

View File

@ -1,17 +1,28 @@
import AdminUser from "admin/models/admin-user";
export default Discourse.Route.extend({
model: function(params) {
this.userFilter = params.filter;
return AdminUser.findAll(params.filter);
queryParams: {
order: { refreshModel: true },
ascending: { refreshModel: true }
},
setupController: function(controller, model) {
controller.setProperties({
model: model,
query: this.userFilter,
showEmails: false,
refreshing: false
});
// TODO: this has been introduced to fix a bug in admin-users-list-show
// loading AdminUser model multiple times without refactoring the controller
beforeModel(transition) {
const routeName = "adminUsersList.show";
if (transition.targetName === routeName) {
const params = transition.params[routeName];
const controller = this.controllerFor(routeName);
if (controller) {
controller.setProperties({
order: transition.queryParams.order,
ascending: transition.queryParams.ascending,
query: params.filter,
showEmails: false,
refreshing: false
});
controller._refreshUsers();
}
}
}
});

View File

@ -0,0 +1,94 @@
<div class='edit-main-nav admin-controls'>
<nav>
<ul class='nav nav-pills target'>
{{#each visibleTargets as |target|}}
<li>
{{#link-to editRouteName
theme.id
target.name
fieldName
replace=true
title=field.title
class=(if target.edited 'edited' 'blank')
}}
{{#if target.error}}{{d-icon 'exclamation-triangle'}}{{/if}}
{{#if target.icon}}
{{d-icon target.icon}}
{{/if}}
{{i18n (concat 'admin.customize.theme.' target.name)}}
{{/link-to}}
</li>
{{/each}}
{{#if allowAdvanced}}
<li>
<a {{action "toggleShowAdvanced"}}
class='no-text'
title="{{i18n (concat "admin.customize.theme." (if showAdvanced "hide_advanced" "show_advanced"))}}"
>
{{d-icon (if showAdvanced "angle-double-left" "angle-double-right")}}
</a>
</li>
{{/if}}
<li class="spacer"></li>
<li>
<label>
{{input type="checkbox" checked=onlyOverridden click=(action "onlyOverriddenChanged" value="target.checked")}}
{{i18n 'admin.customize.theme.hide_unused_fields'}}
</label>
</li>
</ul>
</nav>
</div>
<div class='admin-controls'>
<nav>
<ul class='nav nav-pills fields'>
{{#each visibleFields as |field|}}
<li>
{{#link-to editRouteName
theme.id
currentTargetName
field.name
replace=true
title=field.title
class=(if field.edited 'edited' 'blank')
}}
{{#if field.error}}{{d-icon 'exclamation-triangle'}}{{/if}}
{{#if field.icon}}{{d-icon field.icon}}{{/if}}
{{field.translatedName}}
{{/link-to}}
</li>
{{/each}}
{{#if showAddField}}
<li>
{{#if addingField}}
{{input type=text value=newFieldName enter=(action 'addField') escape-press=(action "cancelAddField")}}
{{d-button class="ok" action=(action "addField" newFieldName) icon="check"}}
{{d-button class="cancel" action=(action "cancelAddField") icon="times"}}
{{else}}
<a {{action "toggleAddField" currentTargetName}} class="no-text">
{{d-icon "plus"}}
</a>
{{/if}}
</li>
{{/if}}
<li class='spacer'></li>
<li>
<a {{action "toggleMaximize"}} class="no-text">
{{d-icon maximizeIcon}}
</a>
</li>
</ul>
</nav>
</div>
{{#if error}}
<pre class='field-error'>{{error}}</pre>
{{/if}}
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}

View File

@ -18,19 +18,19 @@
{{/if}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.editable}} {{i18n 'admin.user_fields.editable.title'}}
{{input type="checkbox" checked=buffered.editable}} {{i18n "admin.user_fields.editable.title"}}
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.required}} {{i18n 'admin.user_fields.required.title'}}
{{input type="checkbox" checked=buffered.required}} {{i18n "admin.user_fields.required.title"}}
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.show_on_profile}} {{i18n 'admin.user_fields.show_on_profile.title'}}
{{input type="checkbox" checked=buffered.show_on_profile}} {{i18n "admin.user_fields.show_on_profile.title"}}
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n 'admin.user_fields.show_on_user_card.title'}}
{{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n "admin.user_fields.show_on_user_card.title"}}
{{/admin-form-row}}
{{#admin-form-row}}
@ -39,15 +39,14 @@
{{/admin-form-row}}
{{else}}
<div class="row">
<div class='form-display'>
<div class="form-display">
<strong>{{userField.name}}</strong>
<br/>
{{{userField.description}}}
</div>
<div class='form-display'>{{fieldName}}</div>
<div class='form-element controls'>
<div class="form-display">{{fieldName}}</div>
<div class="form-element controls">
{{d-button action=(action "edit") class="btn-default" icon="pencil-alt" label="admin.user_fields.edit"}}
{{d-button action=destroyAction actionParam=userField class="btn-danger" icon="far-trash-alt" label="admin.user_fields.delete"}}
{{d-button action=moveUpAction actionParam=userField class="btn-default" icon="arrow-up" disabled=cantMoveUp}}
{{d-button action=moveDownAction actionParam=userField class="btn-default" icon="arrow-down" disabled=cantMoveDown}}
@ -55,4 +54,4 @@
</div>
<div class="row">{{flags}}</div>
{{/if}}
<div class='clearfix'></div>
<div class="clearfix"></div>

View File

@ -7,7 +7,7 @@
<div class="col actions">
{{d-button icon="ellipsis-v" action=(action "toggleRequest") label="admin.web_hooks.events.request"}}
{{d-button icon="ellipsis-v" action=(action "toggleResponse") label="admin.web_hooks.events.response"}}
{{d-button icon="refresh" action=(action "redeliver") label="admin.web_hooks.events.redeliver"}}
{{d-button icon="sync" action=(action "redeliver") label="admin.web_hooks.events.redeliver"}}
</div>
{{#if expandDetails}}
<div class="details">

View File

@ -0,0 +1,8 @@
{{radio-button name="install-items" id=value value=value selection=selection}}
<label class="radio" for="{{value}}">
{{#if showIcon}}
{{d-icon 'plus'}}
{{/if}}
{{i18n label}}
</label>
{{d-icon 'caret-right'}}

View File

@ -1,8 +1,8 @@
<div class="themes-list-header">
<div {{action "changeView" THEMES}} class="themes-tab tab {{if themesTabActive 'active' ''}}">
{{d-icon "cube"}}
{{I18n "admin.customize.theme.title"}}
</div><div {{action "changeView" COMPONENTS}} class="components-tab tab {{if componentsTabActive 'active' ''}}">
{{d-icon "puzzle-piece"}}
{{I18n "admin.customize.theme.components"}}
</div>
</div>
@ -37,4 +37,10 @@
<span class="empty">{{I18n "admin.customize.theme.empty"}}</span>
</div>
{{/if}}
</div>
</div>
<div class="create-actions">
{{d-button action=installModal icon="upload" label="admin.customize.install" class="btn-primary"}}
</div>

View File

@ -1,15 +1,24 @@
<div class='content-list color-schemes'>
<h3>{{i18n 'admin.customize.colors.long_title'}}</h3>
<div class="content-list color-schemes">
<h3>{{i18n "admin.customize.colors.long_title"}}</h3>
<ul>
{{#each model as |scheme|}}
{{#unless scheme.is_base}}
<li>
{{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{d-icon 'paint-brush'}}{{scheme.description}}{{/link-to}}
</li>
<li>
{{#link-to "adminCustomize.colors.show" scheme replace=true}}
{{d-icon "paint-brush"}}
{{scheme.description}}
{{/link-to}}
</li>
{{/unless}}
{{/each}}
</ul>
<button {{action "newColorScheme"}} class='btn btn-default'>{{d-icon 'plus'}}{{i18n 'admin.customize.new'}}</button>
{{d-button
class="btn-default"
action=(action "newColorScheme")
icon="plus"
label="admin.customize.new"}}
</div>
{{outlet}}

View File

@ -1,66 +1,17 @@
<div class="current-style {{if maximized 'maximized'}}">
<div class='wrapper'>
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
{{#if error}}
<pre class='field-error'>{{error}}</pre>
{{/if}}
<div class='edit-main-nav'>
<ul class='nav nav-pills target'>
{{#if showCommon}}
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true}}
{{i18n 'admin.customize.theme.common'}}
{{/link-to}}
</li>
{{/if}}
{{#if showDesktop}}
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true}}
{{i18n 'admin.customize.theme.desktop'}}
{{d-icon 'desktop'}}
{{/link-to}}
</li>
{{/if}}
{{#if showMobile}}
<li class='mobile'>
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true}}
{{i18n 'admin.customize.theme.mobile'}}
{{d-icon 'mobile'}}
{{/link-to}}
</li>
{{/if}}
</ul>
<div class='show-overidden'>
<label>
{{input type="checkbox" checked=onlyOverridden}}
{{i18n 'admin.settings.show_overriden'}}
</label>
</div>
<div class='clearfix'></div>
</div>
<div class='admin-controls'>
<ul class='nav nav-pills fields'>
{{#each fields as |field|}}
<li>
{{#link-to 'adminCustomizeThemes.edit' model.id currentTargetName field.name replace=true title=field.title}}
{{#if field.icon}}{{d-icon field.icon}} {{/if}}
{{i18n field.key}}
{{/link-to}}
</li>
{{/each}}
<li class='toggle-maximize'>
<a {{action "toggleMaximize"}}>
{{d-icon maximizeIcon}}
</a>
</li>
</ul>
</div>
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to showRouteName model.id replace=true}}{{model.name}}{{/link-to}}</h2>
{{admin-theme-editor
theme=model
editRouteName=editRouteName
currentTargetName=currentTargetName
fieldName=fieldName
fieldAdded=(action 'fieldAdded')
maximized=maximized
onlyOverriddenChanged=(action 'onlyOverriddenChanged')
}}
<div class='admin-footer'>
<div class='status-actions'>
{{#unless model.changed}}

View File

@ -3,8 +3,7 @@
<div class="content-wrapper">
<h1>{{I18n "admin.customize.theme.themes_intro"}}</h1>
<div class="create-actions">
{{d-button label="admin.customize.new" icon="plus" action=(route-action "showCreateModal") class="btn-primary"}}
{{d-button action=(route-action "importModal") icon="upload" label="admin.customize.import" class="btn-default"}}
{{d-button action=(route-action "installModal") icon="upload" label="admin.customize.install" class="btn-primary"}}
</div>
<div class="external-resources">
{{#each externalResources as |resource|}}
@ -12,7 +11,7 @@
{{d-icon resource.icon}}
{{I18n resource.key}}
</a>
{{/each}}
{{/each}}
</div>
</div>
</div>

View File

@ -23,7 +23,7 @@
{{i18n "admin.customize.theme.required_version.minimum" version=model.remote_theme.minimum_discourse_version}}
{{/if}}
{{#if model.remote_theme.maximum_discourse_version}}
{{i18n "admin.customize.theme.required_version.minimum" version=model.remote_theme.maximum_discourse_version}}
{{i18n "admin.customize.theme.required_version.maximum" version=model.remote_theme.maximum_discourse_version}}
{{/if}}
</div>
{{/unless}}
@ -38,7 +38,11 @@
{{#if model.remote_theme}}
{{#if model.remote_theme.remote_url}}
<a class="remote-url" href="{{model.remote_theme.remote_url}}">{{i18n "admin.customize.theme.source_url"}} {{d-icon "link"}}</a>
{{#if sourceIsHttp}}
<a class="remote-url" href="{{model.remote_theme.remote_url}}">{{i18n "admin.customize.theme.source_url"}} {{d-icon "link"}}</a>
{{else}}
<div class="remote-url"><code>{{model.remote_theme.remote_url}}</code></div>
{{/if}}
{{/if}}
{{#if model.remote_theme.about_url}}
<a class="url about-url" href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}} {{d-icon "link"}}</a>
@ -67,13 +71,13 @@
<code>{{model.remoteError}}</code>
</div>
{{/if}}
{{#if model.remote_theme.commits_behind}}
{{#d-button action=(action "updateToLatest") icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
{{else}}
{{#d-button action=(action "checkForThemeUpdates") icon="refresh" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
{{#d-button action=(action "checkForThemeUpdates") icon="sync" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
{{/if}}
<span class='status-message'>
{{#if updatingRemote}}
{{i18n 'admin.customize.theme.updating'}}
@ -104,11 +108,14 @@
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.color_scheme"}}</div>
<div class="description">{{i18n "admin.customize.theme.color_scheme_select"}}</div>
<div class="control">{{combo-box content=colorSchemes
filterable=true
forceEscape=true
value=colorSchemeId
icon="paint-brush"}}
<div class="control">
{{color-palettes
content=colorSchemes
filterable=true
forceEscape=true
value=colorSchemeId
icon="paint-brush"}}
{{#if colorSchemeChanged}}
{{d-button action=(action "changeScheme") class="btn-primary submit-edit" icon="check"}}
{{d-button action=(action "cancelChangeScheme") class="btn-default cancel-edit" icon="times"}}
@ -190,7 +197,10 @@
{{#if availableChildThemes}}
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.theme_components"}}</div>
<div class="mini-title">
{{d-icon "puzzle-piece"}}
{{i18n "admin.customize.theme.theme_components"}}
</div>
{{#if model.childThemes.length}}
<ul class='removable-list'>
{{#each model.childThemes as |child|}}

View File

@ -1,12 +1,9 @@
{{#unless editingTheme}}
<div class='customize-themes-header'>
<div class="create-actions">
{{d-button label="admin.customize.new" icon="plus" action=(route-action "showCreateModal") class="btn-primary"}}
{{d-button action=(route-action "importModal") icon="upload" label="admin.customize.import" class="btn-default"}}
</div>
</div>
{{themes-list themes=fullThemes components=childThemes currentTab=currentTab}}
{{themes-list
themes=fullThemes
components=childThemes
currentTab=currentTab
installModal=(route-action "installModal")}}
{{/unless}}
{{outlet}}

View File

@ -18,7 +18,7 @@
</div>
<p class="actions">
{{d-button action=(action "refreshProblems") class="btn-default" icon="refresh" label="admin.dashboard.refresh_problems"}}
{{d-button action=(action "refreshProblems") class="btn-default" icon="sync" label="admin.dashboard.refresh_problems"}}
{{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}}
</p>
{{/conditional-loading-section}}

View File

@ -31,7 +31,9 @@
<td><a {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.logs.none'}}</td></tr>
{{#unless loading}}
<tr><td colspan="4">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/unless}}
{{/each}}
</table>

View File

@ -44,7 +44,9 @@
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{#unless loading}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/unless}}
{{/each}}
</table>

View File

@ -40,7 +40,9 @@
</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{#unless loading}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/unless}}
{{/each}}
</table>

View File

@ -1,49 +1,55 @@
{{#d-modal-body title="admin.badges.preview.modal_title" class="badge-query-preview"}}
{{#if errors}}
<p class="error-header">{{i18n 'admin.badges.preview.sql_error_header'}}</p>
<p class="error-header">{{i18n "admin.badges.preview.sql_error_header"}}</p>
<pre class="badge-errors">{{errors}}</pre>
<!--
TODO we want some help pages for this, link to those instead
<p>
{{i18n 'admin.badges.preview.error_help'}}
{{i18n "admin.badges.preview.error_help"}}
</p>
<ul>
<li><a href="https://meta.discourse.org/t/triggered-custom-badge-queries/19336">https://meta.discourse.org/t/triggered-custom-badge-queries/19336</a></li>
</ul>
-->
{{else}}
<p class="grant-count">
{{#if count}}
{{{i18n 'admin.badges.preview.grant_count' count=count}}}
{{{i18n "admin.badges.preview.grant_count" count=count}}}
{{else}}
{{{i18n 'admin.badges.preview.no_grant_count'}}}
{{{i18n "admin.badges.preview.no_grant_count"}}}
{{/if}}
</p>
{{#if count_warning}}
{{#if countWarning}}
<div class="count-warning">
<p class="heading">{{d-icon "warning"}} {{i18n 'admin.badges.preview.bad_count_warning.header'}}</p>
<p class="body">{{i18n 'admin.badges.preview.bad_count_warning.text'}}</p>
<p class="heading">
{{d-icon "warning"}}
{{i18n "admin.badges.preview.bad_count_warning.header"}}
</p>
<p class="body">
{{i18n "admin.badges.preview.bad_count_warning.text"}}
</p>
</div>
{{/if}}
{{#if sample}}
<p class="sample">
{{i18n 'admin.badges.preview.sample'}}
{{i18n "admin.badges.preview.sample"}}
</p>
<ul>
{{#each processed_sample as |html|}}
{{#each processedSample as |html|}}
<li>{{{html}}}</li>
{{/each}}
</ul>
{{/if}}
{{#if has_query_plan}}
<div class="badge-query-plan">
{{{query_plan_html}}}
</div>
{{#if hasQueryPlan}}
<div class="badge-query-plan">
{{{queryPlanHtml}}}
</div>
{{/if}}
{{/if}}
{{/d-modal-body}}

View File

@ -1,30 +0,0 @@
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.create"}}
<div class="input">
<span class="label">
{{I18n "admin.customize.theme.create_name"}}
</span>
<span class="control">
{{input value=name placeholder=placeholder}}
</span>
</div>
<div class="input">
<span class="label">
{{I18n "admin.customize.theme.create_type"}}
</span>
<span class="control">
{{combo-box valueAttribute="value" content=types value=selectedType}}
</span>
</div>
{{#if showError}}
<div class="error">
{{d-icon "warning"}}
{{I18n "admin.customize.theme.name_too_short"}}
</div>
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action=(action "createTheme") disabled=saving}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>

View File

@ -1,30 +1,34 @@
{{#d-modal-body title="admin.badges.badge_groupings.modal_title" class="badge-groupings-modal"}}
<div class="badge-groupings">
<ul class='badge-groupings-list'>
<ul class="badge-groupings-list">
{{#each workingCopy as |wc|}}
<li class="badge-grouping-item">
<div class="badge-grouping">
{{#if wc.editing}}
{{input value=wc.name class="badge-grouping-name-input"}}
<button {{action "save" wc}} class="btn no-text">{{d-icon 'check'}}</button>
{{d-button action=(action "save" wc) icon="check"}}
{{else}}
<span>{{wc.displayName}}</span>
{{/if}}
</div>
<div class='actions'>
<button {{action "edit" wc}} class="btn no-text" disabled={{wc.system}}>{{d-icon 'pencil-alt'}}</button>
<button {{action "up" wc}} class="btn no-text">{{d-icon 'chevron-up'}}</button>
<button {{action "down" wc}} class="btn no-text">{{d-icon 'chevron-down'}}</button>
<button {{action "delete" wc}} class="btn no-text btn-danger"
disabled={{wc.system}}>{{d-icon 'times'}}</button>
<div class="actions">
{{d-button action=(action "edit" wc) disabled=wc.system icon="pencil-alt"}}
{{d-button action=(action "up" wc) icon="chevron-up"}}
{{d-button action=(action "down" wc) icon="chevron-down"}}
{{d-button action=(action "delete" wc) disabled=wc.system icon="times"}}
</div>
</li>
{{/each}}
</ul>
</div>
<button class='btn new-badge-grouping' {{action "add"}}>{{i18n 'admin.badges.new'}}</button>
{{d-button action=(action "add") label="admin.badges.new"}}
{{/d-modal-body}}
<div class="modal-footer">
<button class='btn btn-primary' {{action "saveAll"}} disabled={{submitDisabled}}>{{i18n 'admin.badges.save'}}</button>
{{d-button
action=(action "saveAll")
label="admin.badges.save"
class="btn-primary"
disabled=submitDisabled}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>
</div>

View File

@ -1,49 +0,0 @@
{{#d-modal-body class='upload-selector' title="admin.customize.theme.import_theme"}}
<div class="radios">
{{radio-button name="upload" id="local" value="local" selection=selection}}
<label class="radio" for="local">{{i18n 'upload_selector.from_my_computer'}}</label>
{{#if local}}
<div class="inputs">
<input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip'><br>
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
</div>
{{/if}}
</div>
<div class="radios">
{{radio-button name="upload" id="remote" value="remote" selection=selection}}
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
{{#if remote}}
<div class="inputs">
<div class='repo'>
{{input value=uploadUrl placeholder=urlPlaceholder}}
<span class="description">{{i18n 'admin.customize.theme.import_web_tip'}}</span>
</div>
<div class='branch'>
{{input value=branch placeholder="master"}}
<span class="description">{{i18n 'admin.customize.theme.remote_branch'}}</span>
</div>
<div class='check-private'>
<label>
{{input type="checkbox" checked=privateChecked}}
{{i18n 'admin.customize.theme.is_private'}}
</label>
</div>
{{#if checkPrivate}}
{{#if privateChecked}}
{{#if publicKey}}
<div class='public-key'>
{{i18n 'admin.customize.theme.public_key'}}
{{textarea readonly=true value=publicKey}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(action "importTheme") disabled=importDisabled class='btn btn-primary' icon='upload' label='admin.customize.import'}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>

View File

@ -0,0 +1,104 @@
{{#d-modal-body class='upload-selector install-theme' title="admin.customize.theme.install"}}
<div class="install-theme-items">
{{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}}
{{install-theme-item value="local" selection=selection label="admin.customize.theme.install_upload"}}
{{install-theme-item value="remote" selection=selection label="admin.customize.theme.install_git_repo"}}
{{install-theme-item value="create" selection=selection label="admin.customize.theme.install_create" showIcon=true}}
</div>
<div class="install-theme-content">
{{#if popular}}
<div class="popular-theme-items">
{{#each themes as |theme|}}
<div class="popular-theme-item">
<div class="popular-theme-name">
<a href="{{theme.meta_url}}" target="_blank">
{{#if theme.component}}
{{d-icon 'puzzle-piece' title='admin.customize.theme.component'}}
{{/if}}
{{theme.name}}
</a>
<div class="popular-theme-description">
{{theme.description}}
</div>
</div>
<div class="popular-theme-buttons">
{{#if theme.installed}}
<span>{{I18n "admin.customize.theme.installed"}}</span>
{{else}}
{{d-button class='btn-small'
label="admin.customize.theme.install"
disabled=installDisabled
icon="upload"
action=(action "installThemeFromList" theme.value)}}
{{#if theme.preview}}
<a href="{{theme.preview}}" target="_blank">{{d-icon "desktop"}} {{I18n "admin.customize.theme.preview"}}</a>
{{/if}}
{{/if}}
</div>
</div>
{{/each}}
</div>
{{/if}}
{{#if local}}
<div class="inputs">
<input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip'><br>
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
</div>
{{/if}}
{{#if remote}}
<div class="inputs">
<div class='repo'>
<div class="label">{{i18n 'admin.customize.theme.import_web_tip'}}</div>
{{input value=uploadUrl placeholder=urlPlaceholder}}
</div>
{{d-button
class="btn-small advanced-repo"
action=(action "toggleAdvanced")
label='admin.customize.theme.import_web_advanced'}}
{{#if advancedVisible}}
<div class='branch'>
<div class="label">{{i18n 'admin.customize.theme.remote_branch'}}</div>
{{input value=branch placeholder="master"}}
</div>
<div class='check-private'>
<label>
{{input type="checkbox" checked=privateChecked}}
{{i18n 'admin.customize.theme.is_private'}}
</label>
</div>
{{#if showPublicKey}}
<div class='public-key'>
<div class="label">{{i18n 'admin.customize.theme.public_key'}}</div>
{{textarea readonly=true value=publicKey}}
</div>
{{/if}}
{{/if}}
</div>
{{/if}}
{{#if create}}
<div class="inputs">
<div class="label">{{I18n "admin.customize.theme.create_name"}}</div>
{{input value=name placeholder=placeholder}}
<div class="label">{{I18n "admin.customize.theme.create_type"}}</div>
{{combo-box valueAttribute="value" content=createTypes value=selectedType}}
</div>
{{/if}}
</div>
{{/d-modal-body}}
{{#unless popular}}
<div class="modal-footer">
{{d-button action=(action "installTheme") disabled=installDisabled class='btn btn-primary' label=submitLabel}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>
{{/unless}}

View File

@ -0,0 +1,19 @@
<div class="popular-theme-item">
<div class="popular-theme-name">
{{theme.name}}
{{#if theme.preview}}
<a href="{{theme.preview}}" title="Preview" target="_blank">{{d-icon "far-eye"}}</a>
{{/if}}
</div>
<div class="popular-theme-buttons">
{{#if theme.installed}}
<span>{{I18n "admin.customize.theme.installed"}}</span>
{{else}}
{{d-button class='btn-small'
label="admin.customize.theme.install"
disabled=installDisabled
icon="upload"
action=(action "installThemeFromList" theme.value)}}
{{/if}}
</div>
</div>

View File

@ -8,7 +8,10 @@
key-up=(action "search")}}
<div class='extra-options'>
{{d-checkbox label="admin.site_text.show_overriden" checked=overridden change=(action "search")}}
<label>
{{input type="checkbox" checked=overridden click=(action "toggleOverridden")}}
{{i18n 'admin.site_text.show_overriden'}}
</label>
</div>
</div>

View File

@ -148,10 +148,6 @@
<div class="value">{{model.ip_address}}</div>
<div class="controls">
{{#if currentUser.staff}}
{{d-button
class="btn-default"
action=(action "refreshBrowsers")
label="admin.user.refresh_browsers"}}
{{ip-lookup ip=model.ip_address userId=model.id}}
{{/if}}
</div>

View File

@ -32,7 +32,11 @@ function resolveParams(ctx, options) {
if (options.hashTypes) {
Object.keys(hash).forEach(function(k) {
const type = options.hashTypes[k];
if (type === "STRING" || type === "StringLiteral") {
if (
type === "STRING" ||
type === "StringLiteral" ||
type === "SubExpression"
) {
params[k] = hash[k];
} else if (type === "ID" || type === "PathExpression") {
params[k] = get(ctx, hash[k], options);

View File

@ -167,7 +167,7 @@ const fa4Replacements = {
"eye-slash": "far-eye-slash",
eyedropper: "eye-dropper",
fa: "fab-font-awesome",
facebook: "fab-facebook-f",
facebook: "fab-facebook",
"facebook-f": "fab-facebook-f",
"facebook-official": "fab-facebook",
"facebook-square": "fab-facebook-square",

View File

@ -1,4 +1,5 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
@ -39,14 +40,12 @@ export default Ember.Component.extend(UploadMixin, {
$upload.on("fileuploadadd", (e, data) => {
ajax("/admin/backups/upload_url", {
data: { filename: data.files[0].name }
}).then(result => {
if (!result.success) {
bootbox.alert(result.message);
} else {
})
.then(result => {
data.url = result.url;
data.submit();
}
});
})
.catch(popupAjaxError);
});
}.on("didInsertElement")
});

View File

@ -1,7 +1,10 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
// A breadcrumb including category drop downs
export default Ember.Component.extend({
classNameBindings: ["hidden:hidden", ":category-breadcrumb"],
tagName: "ol",
parentCategory: Ember.computed.alias("category.parentCategory"),
parentCategories: Ember.computed.filter("categories", function(c) {
@ -12,42 +15,44 @@ export default Ember.Component.extend({
// Don't show "uncategorized" if allow_uncategorized_topics setting is false.
return false;
}
return !c.get("parentCategory");
}),
parentCategoriesSorted: function() {
let cats = this.get("parentCategories");
@computed("parentCategories")
parentCategoriesSorted(parentCategories) {
if (this.siteSettings.fixed_category_positions) {
return cats;
return parentCategories;
}
return cats.sortBy("totalTopicCount").reverse();
}.property("parentCategories"),
return parentCategories.sortBy("totalTopicCount").reverse();
},
hidden: function() {
return this.site.mobileView && !this.get("category");
}.property("category"),
@computed("category")
hidden(category) {
return this.site.mobileView && !category;
},
firstCategory: function() {
return this.get("parentCategory") || this.get("category");
}.property("parentCategory", "category"),
firstCategory: Ember.computed.or("{parentCategory,category}"),
secondCategory: function() {
if (this.get("parentCategory")) return this.get("category");
@computed("category", "parentCategory")
secondCategory(category, parentCategory) {
if (parentCategory) return category;
return null;
}.property("category", "parentCategory"),
},
childCategories: function() {
if (this.get("hideSubcategories")) {
@computed("firstCategory", "hideSubcategories")
childCategories(firstCategory, hideSubcategories) {
if (hideSubcategories) {
return [];
}
var firstCategory = this.get("firstCategory");
if (!firstCategory) {
return [];
}
return this.get("categories").filter(function(c) {
return c.get("parentCategory") === firstCategory;
});
}.property("firstCategory", "hideSubcategories")
return this.get("categories").filter(
c => c.get("parentCategory") === firstCategory
);
}
});

View File

@ -8,6 +8,17 @@ import positioningWorkaround from "discourse/lib/safari-hacks";
import { headerHeight } from "discourse/components/site-header";
import KeyEnterEscape from "discourse/mixins/key-enter-escape";
const START_EVENTS = "touchstart mousedown";
const DRAG_EVENTS = "touchmove mousemove";
const END_EVENTS = "touchend mouseup";
const MIN_COMPOSER_SIZE = 240;
const THROTTLE_RATE = 20;
function mouseYPos(e) {
return e.clientY || (e.touches && e.touches[0] && e.touches[0].clientY);
}
export default Ember.Component.extend(KeyEnterEscape, {
elementId: "reply-control",
@ -84,17 +95,53 @@ export default Ember.Component.extend(KeyEnterEscape, {
}
},
setupComposerResizeEvents() {
const $composer = this.$();
const $grippie = this.$(".grippie");
const $document = Ember.$(document);
let origComposerSize = 0;
let lastMousePos = 0;
const performDrag = event => {
$composer.trigger("div-resizing");
$composer.addClass("clear-transitions");
const currentMousePos = mouseYPos(event);
let size = origComposerSize + (lastMousePos - currentMousePos);
const winHeight = Ember.$(window).height();
size = Math.min(size, winHeight - headerHeight());
size = Math.max(size, MIN_COMPOSER_SIZE);
const sizePx = `${size}px`;
this.movePanels(sizePx);
$composer.height(sizePx);
};
const throttledPerformDrag = (event => {
event.preventDefault();
Ember.run.throttle(this, performDrag, event, THROTTLE_RATE);
}).bind(this);
const endDrag = () => {
$document.off(DRAG_EVENTS, throttledPerformDrag);
$document.off(END_EVENTS, endDrag);
$composer.removeClass("clear-transitions");
$composer.focus();
};
$grippie.on(START_EVENTS, event => {
event.preventDefault();
origComposerSize = $composer.height();
lastMousePos = mouseYPos(event);
$document.on(DRAG_EVENTS, throttledPerformDrag);
$document.on(END_EVENTS, endDrag);
});
},
didInsertElement() {
this._super(...arguments);
const $replyControl = $("#reply-control");
this.setupComposerResizeEvents();
const resize = () => Ember.run(() => this.resize());
$replyControl.DivResizer({
resize,
maxHeight: winHeight => winHeight - headerHeight(),
onDrag: sizePx => this.movePanels(sizePx)
});
const triggerOpen = () => {
if (this.get("composer.composeState") === Composer.OPEN) {
this.appEvents.trigger("composer:opened");
@ -102,13 +149,11 @@ export default Ember.Component.extend(KeyEnterEscape, {
};
triggerOpen();
afterTransition($replyControl, () => {
afterTransition(this.$(), () => {
resize();
triggerOpen();
});
positioningWorkaround(this.$());
this.appEvents.on("composer:resize", this, this.resize);
},
willDestroyElement() {

View File

@ -30,7 +30,8 @@ import {
validateUploadedFiles,
authorizesOneOrMoreImageExtensions,
formatUsername,
clipboardData
clipboardData,
safariHacksDisabled
} from "discourse/lib/utilities";
import {
cacheShortUploadUrl,
@ -108,6 +109,13 @@ export default Ember.Component.extend({
this._resetUpload(true);
},
@observes("focusTarget")
setFocus() {
if (this.get("focusTarget") === "editor") {
this.$("textarea").putCursorAtEnd();
}
},
@computed
markdownOptions() {
return {
@ -185,8 +193,19 @@ export default Ember.Component.extend({
);
}
if (!this.site.mobileView) {
$preview
.off("touchstart mouseenter", "img")
.on("touchstart mouseenter", "img", () => {
this._placeImageScaleButtons($preview);
});
}
// Focus on the body unless we have a title
if (!this.get("composer.canEditTitle") && !this.capabilities.isIOS) {
if (
!this.get("composer.canEditTitle") &&
(!this.capabilities.isIOS || safariHacksDisabled())
) {
this.$(".d-editor-input").putCursorAtEnd();
}
@ -767,6 +786,116 @@ export default Ember.Component.extend({
}
},
_appendImageScaleButtons($images, imageScaleRegex) {
const buttonScales = [100, 75, 50];
const imageWrapperTemplate = `<div class="image-wrapper"></div>`;
const buttonWrapperTemplate = `<div class="button-wrapper"></div>`;
const scaleButtonTemplate = `<span class="scale-btn"></a>`;
$images.each((i, e) => {
const $e = $(e);
const matches = this.get("composer.reply").match(imageScaleRegex);
// ignore previewed upload markdown in codeblock
if (!matches || $e.hasClass("codeblock-image")) return;
if (!$e.parent().hasClass("image-wrapper")) {
const match = matches[i];
const matchingPlaceholder = imageScaleRegex.exec(match);
if (!matchingPlaceholder) return;
const currentScale = matchingPlaceholder[2] || 100;
$e.data("index", i).wrap(imageWrapperTemplate);
$e.parent().append(
$(buttonWrapperTemplate).attr("data-image-index", i)
);
buttonScales.forEach((buttonScale, buttonIndex) => {
const activeClass =
parseInt(currentScale, 10) === buttonScale ? "active" : "";
const $scaleButton = $(scaleButtonTemplate)
.addClass(activeClass)
.attr("data-scale", buttonScale)
.text(`${buttonScale}%`);
const $buttonWrapper = $e.parent().find(".button-wrapper");
$buttonWrapper.append($scaleButton);
if (buttonIndex !== buttonScales.length - 1) {
$buttonWrapper.append(`<span class="separator"> | </span>`);
}
});
}
});
},
_registerImageScaleButtonClick($preview, imageScaleRegex) {
$preview.off("click", ".scale-btn").on("click", ".scale-btn", e => {
const index = parseInt(
$(e.target)
.parent()
.attr("data-image-index")
);
const scale = e.target.attributes["data-scale"].value;
const matchingPlaceholder = this.get("composer.reply").match(
imageScaleRegex
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
if (!match) {
return;
}
const replacement = match.replace(imageScaleRegex, `$1,${scale}%$3`);
this.appEvents.trigger(
"composer:replace-text",
matchingPlaceholder[index],
replacement,
{ regex: imageScaleRegex, index }
);
}
});
},
_placeImageScaleButtons($preview) {
// regex matches only upload placeholders with size defined,
// which is required for resizing
// original string `![28|690x226,5%](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
// match 1 `![28|690x226`
// match 2 `5`
// match 3 `](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
const imageScaleRegex = /(!\[(?:\S*?(?=\|)\|)*?(?:\d{1,6}x\d{1,6})+?)(?:,?(\d{1,3})?%?)?(\]\(upload:\/\/\S*?\))/g;
// wraps previewed upload markdown in a codeblock in its own class to keep a track
// of indexes later on to replace the correct upload placeholder in the composer
if ($preview.find(".codeblock-image").length === 0) {
this.$(".d-editor-preview *")
.contents()
.filter(function() {
return this.nodeType === 3; // TEXT_NODE
})
.each(function() {
$(this).replaceWith(
$(this)
.text()
.replace(imageScaleRegex, "<span class='codeblock-image'>$&</a>")
);
});
}
const $images = $preview.find("img.resizable, span.codeblock-image");
this._appendImageScaleButtons($images, imageScaleRegex);
this._registerImageScaleButtonClick($preview, imageScaleRegex);
},
@on("willDestroyElement")
_unbindUploadTarget() {
this._validUploads = 0;
@ -804,6 +933,12 @@ export default Ember.Component.extend({
this.storeToolbarState(toolbarEvent);
},
showPreview() {
const $preview = this.$(".d-editor-preview-wrapper");
this._placeImageScaleButtons($preview);
this.send("togglePreview");
},
actions: {
importQuote(toolbarEvent) {
this.importQuote(toolbarEvent);
@ -852,7 +987,7 @@ export default Ember.Component.extend({
group: "mobileExtras",
icon: "television",
title: "composer.show_preview",
sendAction: this.get("togglePreview")
sendAction: this.showPreview.bind(this)
});
}
},
@ -960,6 +1095,10 @@ export default Ember.Component.extend({
);
}
if (this.site.mobileView && $preview.is(":visible")) {
this._placeImageScaleButtons($preview);
}
this.trigger("previewRefreshed", $preview);
this.afterRefresh($preview);
}

View File

@ -16,10 +16,22 @@ export default Ember.Component.extend({
return false;
}
});
this.$().on("click.dropdown-user-field-label", "[for]", event => {
const $element = $(event.target);
const $target = $(`#${$element.attr("for")}`);
if ($target.is(".select-kit")) {
event.preventDefault();
$target.find(".select-kit-header").trigger("click");
}
});
},
willDestroyElement() {
this._super(...arguments);
this.$().off("keydown.discourse-create-account");
this.$().off("click.dropdown-user-field-label");
}
});

View File

@ -4,9 +4,12 @@ export default Ember.Component.extend({
// subclasses need this
layoutName: "components/d-button",
form: null,
tagName: "button",
classNameBindings: [":btn", "noText", "btnType"],
attributeBindings: [
"form",
"disabled",
"translatedTitle:title",
"translatedLabel:aria-label",

View File

@ -1,25 +0,0 @@
import { on } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
tagName: "label",
@on("didInsertElement")
_init() {
const checked = this.get("checked");
if (checked && checked !== "false") {
this.$("input").prop("checked", true);
}
// In Ember 13.3 we can use action on the checkbox `{{input}}` but not in 1.11
this.$("input").on("click.d-checkbox", () => {
Ember.run.scheduleOnce("afterRender", () =>
this.change(this.$("input").prop("checked"))
);
});
},
@on("willDestroyElement")
_clear() {
this.$("input").off("click.d-checkbox");
}
});

View File

@ -12,7 +12,8 @@ import { findRawTemplate } from "discourse/lib/raw-templates";
import { siteDir } from "discourse/lib/text-direction";
import {
determinePostReplaceSelection,
clipboardData
clipboardData,
safariHacksDisabled
} from "discourse/lib/utilities";
import toMarkdown from "discourse/lib/to-markdown";
import deprecated from "discourse-common/lib/deprecated";
@ -80,7 +81,7 @@ class Toolbar {
icon: "italic",
label: getButtonLabel("composer.italic_label", "I"),
shortcut: "I",
perform: e => e.applySurround("_", "_", "italic_text")
perform: e => e.applySurround("*", "*", "italic_text")
});
if (opts.showLink) {
@ -256,15 +257,6 @@ export default Ember.Component.extend({
const mouseTrap = Mousetrap(this.$(".d-editor-input")[0]);
const shortcuts = this.get("toolbar.shortcuts");
// for some reason I am having trouble bubbling this so hack it in
mouseTrap.bind(["ctrl+alt+f"], event => {
this.appEvents.trigger("header:keyboard-trigger", {
type: "search",
event
});
return true;
});
Object.keys(shortcuts).forEach(sc => {
const button = shortcuts[sc];
mouseTrap.bind(sc, () => {
@ -304,8 +296,8 @@ export default Ember.Component.extend({
this.appEvents.on("composer:insert-text", (text, options) =>
this._addText(this._getSelected(), text, options)
);
this.appEvents.on("composer:replace-text", (oldVal, newVal) =>
this._replaceText(oldVal, newVal)
this.appEvents.on("composer:replace-text", (oldVal, newVal, opts) =>
this._replaceText(oldVal, newVal, opts)
);
}
this._mouseTrap = mouseTrap;
@ -323,7 +315,6 @@ export default Ember.Component.extend({
Object.keys(this.get("toolbar.shortcuts")).forEach(sc =>
mouseTrap.unbind(sc)
);
mouseTrap.unbind("ctrl+/", "command+/");
this.$(".d-editor-preview").off("click.preview");
},
@ -534,7 +525,7 @@ export default Ember.Component.extend({
const $textarea = this.$("textarea.d-editor-input");
const textarea = $textarea[0];
const oldScrollPos = $textarea.scrollTop();
if (!this.capabilities.isIOS) {
if (!this.capabilities.isIOS || safariHacksDisabled()) {
$textarea.focus();
}
textarea.selectionStart = from;
@ -669,7 +660,7 @@ export default Ember.Component.extend({
}
},
_replaceText(oldVal, newVal) {
_replaceText(oldVal, newVal, opts) {
const val = this.get("value");
const needleStart = val.indexOf(oldVal);
@ -687,8 +678,17 @@ export default Ember.Component.extend({
replacement: { start: needleStart, end: needleStart + newVal.length }
});
// Replace value (side effect: cursor at the end).
this.set("value", val.replace(oldVal, newVal));
if (opts && opts.index && opts.regex) {
let i = -1;
const newValue = val.replace(opts.regex, match => {
i++;
return i === opts.index ? newVal : match;
});
this.set("value", newValue);
} else {
// Replace value (side effect: cursor at the end).
this.set("value", val.replace(oldVal, newVal));
}
if ($("textarea.d-editor-input").is(":focus")) {
// Restore cursor.

View File

@ -1,9 +1,17 @@
import { on } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
classNameBindings: [":modal", ":d-modal", "modalClass", "modalStyle"],
classNameBindings: [
":modal",
":d-modal",
"modalClass",
"modalStyle",
"hasPanels"
],
attributeBindings: ["data-keyboard"],
dismissable: true,
title: null,
subtitle: null,
init() {
this._super(...arguments);

View File

@ -7,6 +7,12 @@ export default Ember.Component.extend(UploadMixin, {
hasName: Ember.computed.notEmpty("name"),
addDisabled: Ember.computed.not("hasName"),
uploadOptions() {
return {
sequentialUploads: true
};
},
@computed("hasName", "name")
data(hasName, name) {
return hasName ? { name } : {};

View File

@ -35,7 +35,7 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
@computed("group")
groupPath(group) {
return `${Discourse.BaseUri}/groups/${group.name}`;
return `${Discourse.BaseUri}/g/${group.name}`;
},
_showCallback(username, $target) {

View File

@ -30,6 +30,7 @@ export default Ember.Component.extend({
? []
: [groupNames],
single: this.get("single"),
fullWidthWrap: this.get("fullWidthWrap"),
updateData: opts && opts.updateData ? opts.updateData : false,
onChangeItems: items => {
selectedGroups = items;

View File

@ -1,8 +1,22 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
import lightbox from "discourse/lib/lightbox";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Ember.Component.extend(UploadMixin, {
classNames: ["image-uploader"],
loadingLightbox: false,
init() {
this._super(...arguments);
this._applyLightbox();
},
willDestroyElement() {
this._super(...arguments);
$("a.lightbox").magnificPopup("close");
},
@computed("imageUrl")
backgroundStyle(imageUrl) {
@ -13,6 +27,12 @@ export default Ember.Component.extend(UploadMixin, {
return `background-image: url(${imageUrl})`.htmlSafe();
},
@computed("imageUrl")
imageBaseName(imageUrl) {
if (Ember.isEmpty(imageUrl)) return;
return imageUrl.split("/").slice(-1)[0];
},
@computed("backgroundStyle")
hasBackgroundStyle(backgroundStyle) {
return !Ember.isEmpty(backgroundStyle.string);
@ -23,14 +43,56 @@ export default Ember.Component.extend(UploadMixin, {
},
uploadDone(upload) {
this.setProperties({ imageUrl: upload.url, imageId: upload.id });
this.setProperties({
imageUrl: upload.url,
imageId: upload.id,
imageFilesize: upload.human_filesize,
imageFilename: upload.original_filename,
imageWidth: upload.width,
imageHeight: upload.height
});
this._applyLightbox();
if (this.onUploadDone) {
this.onUploadDone(upload);
}
},
_openLightbox() {
Ember.run.next(() => this.$("a.lightbox").magnificPopup("open"));
},
_applyLightbox() {
if (this.get("imageUrl")) Ember.run.next(() => lightbox(this.$()));
},
actions: {
toggleLightbox() {
if (this.get("imageFilename")) {
this._openLightbox();
} else {
this.set("loadingLightbox", true);
ajax(`/uploads/lookup-metadata`, {
type: "POST",
data: { url: this.get("imageUrl") }
})
.then(json => {
this.setProperties({
imageFilename: json.original_filename,
imageFilesize: json.human_filesize,
imageWidth: json.width,
imageHeight: json.height
});
this._openLightbox();
this.set("loadingLightbox", false);
})
.catch(popupAjaxError);
}
},
trash() {
this.setProperties({ imageUrl: null, imageId: null });

View File

@ -1,34 +1,30 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { emailValid } from "discourse/lib/utilities";
import computed from "ember-addons/ember-computed-decorators";
import Group from "discourse/models/group";
import Invite from "discourse/models/invite";
import { i18n } from "discourse/lib/computed";
export default Ember.Controller.extend(ModalFunctionality, {
userInvitedShow: Ember.inject.controller("user-invited-show"),
export default Ember.Component.extend({
tagName: null,
// If this isn't defined, it will proxy to the user model on the preferences
inviteModel: Ember.computed.alias("panel.model.inviteModel"),
userInvitedShow: Ember.computed.alias("panel.model.userInvitedShow"),
// If this isn't defined, it will proxy to the user topic on the preferences
// page which is wrong.
emailOrUsername: null,
hasCustomMessage: false,
hasCustomMessage: false,
customMessage: null,
inviteIcon: "envelope",
invitingExistingUserToTopic: false,
@computed("isMessage", "invitingToTopic")
title(isMessage, invitingToTopic) {
if (isMessage) {
return "topic.invite_private.title";
} else if (invitingToTopic) {
return "topic.invite_reply.title";
} else {
return "user.invited.create";
}
},
isAdmin: Ember.computed.alias("currentUser.admin"),
@computed
isAdmin() {
return this.currentUser.admin;
willDestroyElement() {
this._super(...arguments);
this.reset();
},
@computed(
@ -36,9 +32,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
"emailOrUsername",
"invitingToTopic",
"isPrivateTopic",
"model.groupNames",
"model.saving",
"model.details.can_invite_to"
"topic.groupNames",
"topic.saving",
"topic.details.can_invite_to"
)
disabled(
isAdmin,
@ -51,26 +47,39 @@ export default Ember.Controller.extend(ModalFunctionality, {
) {
if (saving) return true;
if (Ember.isEmpty(emailOrUsername)) return true;
const emailTrimmed = emailOrUsername.trim();
// when inviting to forum, email must be valid
if (!invitingToTopic && !emailValid(emailTrimmed)) return true;
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) return true;
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(emailTrimmed))
if (!invitingToTopic && !emailValid(emailTrimmed)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (
isPrivateTopic &&
Ember.isEmpty(groupNames) &&
emailValid(emailTrimmed)
) {
return true;
}
if (can_invite_to) return false;
return false;
},
@computed(
"isAdmin",
"emailOrUsername",
"model.saving",
"inviteModel.saving",
"isPrivateTopic",
"model.groupNames",
"inviteModel.groupNames",
"hasCustomMessage"
)
disabledCopyLink(
@ -84,54 +93,65 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (hasCustomMessage) return true;
if (saving) return true;
if (Ember.isEmpty(emailOrUsername)) return true;
const email = emailOrUsername.trim();
// email must be valid
if (!emailValid(email)) return true;
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(email)) return true;
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email))
if (!emailValid(email)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(email)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) {
return true;
}
return false;
},
@computed("model.saving")
@computed("inviteModel.saving")
buttonTitle(saving) {
return saving ? "topic.inviting" : "topic.invite_reply.action";
},
// We are inviting to a topic if the model isn't the current user.
// We are inviting to a topic if the topic isn't the current user.
// The current user would mean we are inviting to the forum in general.
@computed("model")
invitingToTopic(model) {
return model !== this.currentUser;
@computed("inviteModel")
invitingToTopic(inviteModel) {
return inviteModel !== this.currentUser;
},
@computed("model", "model.details.can_invite_via_email")
canInviteViaEmail(model, can_invite_via_email) {
return this.get("model") === this.currentUser ? true : can_invite_via_email;
@computed("inviteModel", "inviteModel.details.can_invite_via_email")
canInviteViaEmail(inviteModel, canInviteViaEmail) {
return this.get("inviteModel") === this.currentUser
? true
: canInviteViaEmail;
},
@computed("isMessage", "canInviteViaEmail")
showCopyInviteButton(isMessage, canInviteViaEmail) {
return canInviteViaEmail && !isMessage;
@computed("isPM", "canInviteViaEmail")
showCopyInviteButton(isPM, canInviteViaEmail) {
return canInviteViaEmail && !isPM;
},
topicId: Ember.computed.alias("model.id"),
topicId: Ember.computed.alias("inviteModel.id"),
// Is Private Topic? (i.e. visible only to specific group members)
// eg: visible only to specific group members
isPrivateTopic: Ember.computed.and(
"invitingToTopic",
"model.category.read_restricted"
"inviteModel.category.read_restricted"
),
// Is Private Message?
isMessage: Ember.computed.equal("model.archetype", "private_message"),
isPM: Ember.computed.equal("inviteModel.archetype", "private_message"),
// Allow Existing Members? (username autocomplete)
// scope to allowed usernames
allowExistingMembers: Ember.computed.alias("invitingToTopic"),
@computed("isAdmin", "model.group_users")
@computed("isAdmin", "inviteModel.group_users")
isGroupOwnerOrAdmin(isAdmin, groupUsers) {
return (
isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner))
@ -143,7 +163,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
"isGroupOwnerOrAdmin",
"emailOrUsername",
"isPrivateTopic",
"isMessage",
"isPM",
"invitingToTopic",
"canInviteViaEmail"
)
@ -151,14 +171,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
isGroupOwnerOrAdmin,
emailOrUsername,
isPrivateTopic,
isMessage,
isPM,
invitingToTopic,
canInviteViaEmail
) {
return (
isGroupOwnerOrAdmin &&
canInviteViaEmail &&
!isMessage &&
!isPM &&
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic)
);
},
@ -166,13 +186,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
@computed("emailOrUsername")
showCustomMessage(emailOrUsername) {
return (
this.get("model") === this.currentUser || emailValid(emailOrUsername)
this.get("inviteModel") === this.currentUser ||
emailValid(emailOrUsername)
);
},
// Instructional text for the modal.
@computed(
"isMessage",
"isPM",
"invitingToTopic",
"emailOrUsername",
"isPrivateTopic",
@ -180,7 +201,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
"canInviteViaEmail"
)
inviteInstructions(
isMessage,
isPM,
invitingToTopic,
emailOrUsername,
isPrivateTopic,
@ -190,7 +211,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (!canInviteViaEmail) {
// can't invite via email, only existing users
return I18n.t("topic.invite_reply.sso_enabled");
} else if (isMessage) {
} else if (isPM) {
// inviting to a message
return I18n.t("topic.invite_private.email_or_username");
} else if (invitingToTopic) {
@ -222,14 +243,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
groupFinder(term) {
return Group.findAll({ term: term, ignore_automatic: true });
return Group.findAll({ term, ignore_automatic: true });
},
@computed("isMessage", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) {
@computed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) {
if (this.get("hasGroups")) {
return I18n.t("topic.invite_private.success_group");
} else if (isMessage) {
} else if (isPM) {
return I18n.t("topic.invite_private.success");
} else if (invitingExistingUserToTopic) {
return I18n.t("topic.invite_reply.success_existing_email", {
@ -242,9 +263,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
},
@computed("isMessage")
errorMessage(isMessage) {
return isMessage
@computed("isPM")
errorMessage(isPM) {
return isPM
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error");
},
@ -256,18 +277,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
: "topic.invite_reply.username_placeholder";
},
@computed
customMessagePlaceholder() {
return I18n.t("invite.custom_message_placeholder");
},
customMessagePlaceholder: i18n("invite.custom_message_placeholder"),
// Reset the modal to allow a new user to be invited.
reset() {
this.set("emailOrUsername", null);
this.set("hasCustomMessage", false);
this.set("customMessage", null);
this.set("invitingExistingUserToTopic", false);
this.get("model").setProperties({
this.setProperties({
emailOrUsername: null,
hasCustomMessage: false,
customMessage: null,
invitingExistingUserToTopic: false
});
this.get("inviteModel").setProperties({
groupNames: null,
error: false,
saving: false,
@ -278,24 +299,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
actions: {
createInvite() {
const self = this;
if (this.get("disabled")) {
return;
}
const groupNames = this.get("model.groupNames"),
userInvitedController = this.get("userInvitedShow"),
model = this.get("model");
const groupNames = this.get("inviteModel.groupNames");
const userInvitedController = this.get("userInvitedShow");
const model = this.get("inviteModel");
model.setProperties({ saving: true, error: false });
const onerror = e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
this.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set(
this.set(
"errorMessage",
self.get("isMessage")
this.get("isPM")
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error")
);
@ -304,18 +324,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
};
if (this.get("hasGroups")) {
return this.get("model")
return this.get("inviteModel")
.createGroupInvite(this.get("emailOrUsername").trim())
.then(data => {
model.setProperties({ saving: false, finished: true });
this.get("model.details.allowed_groups").pushObject(
this.get("inviteModel.details.allowed_groups").pushObject(
Ember.Object.create(data.group)
);
this.appEvents.trigger("post-stream:refresh");
})
.catch(onerror);
} else {
return this.get("model")
return this.get("inviteModel")
.createInvite(
this.get("emailOrUsername").trim(),
groupNames,
@ -323,19 +343,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
)
.then(result => {
model.setProperties({ saving: false, finished: true });
if (!this.get("invitingToTopic")) {
if (!this.get("invitingToTopic") && userInvitedController) {
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(invite_model => {
userInvitedController.set("model", invite_model);
userInvitedController.set(
"totalInvites",
invite_model.invites.length
);
).then(inviteModel => {
userInvitedController.setProperties({
model: inviteModel,
totalInvites: inviteModel.invites.length
});
});
} else if (this.get("isMessage") && result && result.user) {
this.get("model.details.allowed_users").pushObject(
} else if (this.get("isPM") && result && result.user) {
this.get("inviteModel.details.allowed_users").pushObject(
Ember.Object.create(result.user)
);
this.appEvents.trigger("post-stream:refresh");
@ -353,24 +372,21 @@ export default Ember.Controller.extend(ModalFunctionality, {
},
generateInvitelink() {
const self = this;
if (this.get("disabled")) {
return;
}
const groupNames = this.get("model.groupNames"),
userInvitedController = this.get("userInvitedShow"),
model = this.get("model");
var topicId = null;
if (this.get("invitingToTopic")) {
topicId = this.get("model.id");
}
const groupNames = this.get("inviteModel.groupNames");
const userInvitedController = this.get("userInvitedShow");
const model = this.get("inviteModel");
model.setProperties({ saving: true, error: false });
return this.get("model")
let topicId;
if (this.get("invitingToTopic")) {
topicId = this.get("inviteModel.id");
}
return model
.generateInviteLink(
this.get("emailOrUsername").trim(),
groupNames,
@ -382,24 +398,26 @@ export default Ember.Controller.extend(ModalFunctionality, {
finished: true,
inviteLink: result
});
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(invite_model => {
userInvitedController.set("model", invite_model);
userInvitedController.set(
"totalInvites",
invite_model.invites.length
);
});
if (userInvitedController) {
Invite.findInvitedBy(
this.currentUser,
userInvitedController.get("filter")
).then(inviteModel => {
userInvitedController.setProperties({
model: inviteModel,
totalInvites: inviteModel.invites.length
});
});
}
})
.catch(function(e) {
.catch(e => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
this.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set(
this.set(
"errorMessage",
self.get("isMessage")
this.get("isPM")
? I18n.t("topic.invite_private.error")
: I18n.t("topic.invite_reply.error")
);
@ -411,7 +429,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
showCustomMessageBox() {
this.toggleProperty("hasCustomMessage");
if (this.get("hasCustomMessage")) {
if (this.get("model") === this.currentUser) {
if (this.get("inviteModel") === this.currentUser) {
this.set(
"customMessage",
I18n.t("invite.custom_message_template_forum")

View File

@ -0,0 +1,11 @@
import { fmt } from "discourse/lib/computed";
export default Ember.Component.extend({
panel: null,
panelComponent: fmt("panel.id", "%@-panel"),
classNameBindings: ["panel.id"],
classNames: ["modal-panel"]
});

View File

@ -0,0 +1,17 @@
import { propertyEqual } from "discourse/lib/computed";
export default Ember.Component.extend({
tagName: "li",
classNames: ["modal-tab"],
panel: null,
selectedPanel: null,
panelsLength: null,
classNameBindings: ["isActive", "singleTab", "panel.id"],
singleTab: Ember.computed.equal("panelsLength", 1),
title: Ember.computed.alias("panel.title"),
isActive: propertyEqual("panel.id", "selectedPanel.id"),
click() {
this.onSelectPanel(this.get("panel"));
}
});

View File

@ -16,17 +16,23 @@ export default Ember.Component.extend({
: I18n.t("login.second_factor_backup_description");
},
@computed("secondFactorMethod")
linkText(secondFactorMethod) {
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
? "login.second_factor_backup"
: "login.second_factor";
@computed("secondFactorMethod", "isLogin")
linkText(secondFactorMethod, isLogin) {
if (isLogin) {
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
? "login.second_factor_backup"
: "login.second_factor";
} else {
return secondFactorMethod === SECOND_FACTOR_METHODS.TOTP
? "user.second_factor_backup.use"
: "user.second_factor.use";
}
},
actions: {
toggleSecondFactorMethod() {
const secondFactorMethod = this.get("secondFactorMethod");
this.set("loginSecondFactor", "");
this.set("secondFactorToken", "");
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) {
this.set("secondFactorMethod", SECOND_FACTOR_METHODS.BACKUP_CODE);
} else {

View File

@ -0,0 +1,75 @@
import { escapeExpression } from "discourse/lib/utilities";
import { default as computed } from "ember-addons/ember-computed-decorators";
import Sharing from "discourse/lib/sharing";
export default Ember.Component.extend({
tagName: null,
type: Ember.computed.alias("panel.model.type"),
topic: Ember.computed.alias("panel.model.topic"),
@computed
sources() {
return Sharing.activeSources(this.siteSettings.share_links);
},
@computed("type", "topic.title")
shareTitle(type, topicTitle) {
topicTitle = escapeExpression(topicTitle);
return I18n.t("share.topic_html", { topicTitle });
},
@computed("panel.model.shareUrl", "topic.shareUrl")
shareUrl(forcedShareUrl, shareUrl) {
shareUrl = forcedShareUrl || shareUrl;
if (Ember.isEmpty(shareUrl)) {
return;
}
// Relative urls
if (shareUrl.indexOf("/") === 0) {
const location = window.location;
shareUrl = `${location.protocol}//${location.host}${shareUrl}`;
}
return encodeURI(shareUrl);
},
didInsertElement() {
this._super(...arguments);
const shareUrl = this.get("shareUrl");
const $linkInput = this.$(".topic-share-url");
const $linkForTouch = this.$(".topic-share-url-for-touch a");
Ember.run.schedule("afterRender", () => {
if (!this.capabilities.touch) {
$linkForTouch.parent().remove();
$linkInput
.val(shareUrl)
.select()
.focus();
} else {
$linkInput.remove();
$linkForTouch.attr("href", shareUrl).text(shareUrl);
const range = window.document.createRange();
range.selectNode($linkForTouch[0]);
window.getSelection().addRange(range);
}
});
},
actions: {
share(source) {
Sharing.shareSource(source, {
url: this.get("shareUrl"),
title: this.get("topic.title")
});
}
}
});

View File

@ -2,6 +2,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
import { longDateNoYear } from "discourse/lib/formatter";
import computed from "ember-addons/ember-computed-decorators";
import Sharing from "discourse/lib/sharing";
import { nativeShare } from "discourse/lib/pwa-utils";
export default Ember.Component.extend({
elementId: "share-link",
@ -87,11 +88,6 @@ export default Ember.Component.extend({
Ember.run.scheduleOnce("afterRender", this, this._focusUrl);
},
_webShare(url) {
// We can pass title and text too, but most share targets do their own oneboxing
return navigator.share({ url });
},
didInsertElement() {
this._super(...arguments);
@ -126,12 +122,10 @@ export default Ember.Component.extend({
this.setProperties({ postNumber, date, postId });
// use native webshare only when the user clicks on the "chain" icon
// navigator.share needs HTTPS, returns undefined on HTTP
if (navigator.share && !$currentTarget.hasClass("post-date")) {
this._webShare(url).catch(() => {
// if navigator fails for unexpected reason fallback to popup
this._showUrl($currentTarget, url);
});
if (!$currentTarget.hasClass("post-date")) {
nativeShare({ url }).then(null, () =>
this._showUrl($currentTarget, url)
);
} else {
this._showUrl($currentTarget, url);
}
@ -160,15 +154,6 @@ export default Ember.Component.extend({
},
actions: {
replyAsNewTopic() {
const postStream = this.get("topic.postStream");
const postId =
this.get("postId") || postStream.findPostIdForPostNumber(1);
const post = postStream.findLoadedPost(postId);
this.get("replyAsNewTopic")(post);
this.send("close");
},
close() {
this.setProperties({
link: null,
@ -179,17 +164,10 @@ export default Ember.Component.extend({
},
share(source) {
const url = source.generateUrl(this.get("link"), this.get("topic.title"));
if (source.shouldOpenInPopup) {
window.open(
url,
"",
"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=" +
(source.popupHeight || 315)
);
} else {
window.open(url, "_blank");
}
Sharing.shareSource(source, {
url: this.get("link"),
title: this.get("topic.title")
});
}
}
});

View File

@ -15,7 +15,7 @@ function addFlagProperty(prop) {
const PANEL_BODY_MARGIN = 30;
//android supports pulling in from the screen edges
const SCREEN_EDGE_MARGIN = 30;
const SCREEN_EDGE_MARGIN = 20;
const SCREEN_OFFSET = 300;
const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
@ -228,6 +228,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
didInsertElement() {
this._super(...arguments);
const { isAndroid } = this.capabilities;
$(window).on("resize.discourse-menu-panel", () => this.afterRender());
this.appEvents.on("header:show-topic", topic => this.setTopic(topic));
@ -244,11 +245,16 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
}
});
this.addTouchListeners($("body"));
// Only add listeners for opening menus by swiping them in on Android devices
// iOS will respond to these events, but also does swiping for back/forward
if (isAndroid) {
this.addTouchListeners($("body"));
}
},
willDestroyElement() {
this._super(...arguments);
const { isAndroid } = this.capabilities;
$("body").off("keydown.header");
$(window).off("resize.discourse-menu-panel");
@ -256,7 +262,9 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
this.appEvents.off("header:hide-topic");
this.appEvents.off("dom:clean");
this.removeTouchListeners($("body"));
if (isAndroid) {
this.removeTouchListeners($("body"));
}
Ember.run.cancel(this._scheduledRemoveAnimate);
window.cancelAnimationFrame(this._scheduledMovingAnimation);

View File

@ -30,10 +30,7 @@ export default Ember.Component.extend({
return !isPM || this.siteSettings.enable_personal_messages;
},
@computed("topic.details.can_invite_to")
canInviteTo(result) {
return !this.site.mobileView && result;
},
canInviteTo: Ember.computed.alias("topic.details.can_invite_to"),
inviteDisabled: Ember.computed.or(
"topic.archived",

View File

@ -155,14 +155,16 @@ export default Ember.Component.extend({
const $wrapper = this.$();
if (!$wrapper || $wrapper.length === 0) return;
const offset = window.pageYOffset || $("html").scrollTop(),
const $html = $("html"),
offset = window.pageYOffset || $html.scrollTop(),
progressHeight = this.site.mobileView ? 0 : $("#topic-progress").height(),
maximumOffset = $("#topic-bottom").offset().top + progressHeight,
windowHeight = $(window).height(),
bodyHeight = $("body").height(),
composerHeight = $("#reply-control").height() || 0,
isDocked = offset >= maximumOffset - windowHeight + composerHeight,
bottom = bodyHeight - maximumOffset;
bottom = bodyHeight - maximumOffset,
wrapperDir = $html.hasClass("rtl") ? "left" : "right";
if (composerHeight > 0) {
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
@ -174,9 +176,9 @@ export default Ember.Component.extend({
const $replyArea = $("#reply-control .reply-area");
if ($replyArea && $replyArea.length > 0) {
$wrapper.css("right", `${$replyArea.offset().left}px`);
$wrapper.css(wrapperDir, `${$replyArea.offset().left}px`);
} else {
$wrapper.css("right", "1em");
$wrapper.css(wrapperDir, "1em");
}
// switch mobile scroll logo at the very bottom of topics

View File

@ -195,6 +195,16 @@ export default Ember.Component.extend(
this._close();
},
ignoreUser() {
this.get("user").ignore();
this._close();
},
watchUser() {
this.get("user").watch();
this._close();
},
showUser() {
this.showUser(this.get("user"));
this._close();

View File

@ -34,7 +34,8 @@ export default TextField.extend({
single = bool("single"),
allowAny = bool("allowAny"),
disabled = bool("disabled"),
disallowEmails = bool("disallowEmails");
allowEmails = bool("allowEmails"),
fullWidthWrap = bool("fullWidthWrap");
function excludedUsernames() {
// hack works around some issues with allowAny eventing
@ -54,6 +55,7 @@ export default TextField.extend({
single: single,
allowAny: allowAny,
updateData: opts && opts.updateData ? opts.updateData : false,
fullWidthWrap,
dataSource(term) {
var results = userSearch({
@ -65,7 +67,7 @@ export default TextField.extend({
includeMentionableGroups,
includeMessageableGroups,
group: self.get("group"),
disallowEmails
allowEmails
});
return results;
},

View File

@ -12,7 +12,8 @@ import { getOwner } from "discourse-common/lib/get-owner";
import {
escapeExpression,
uploadIcon,
authorizesOneOrMoreExtensions
authorizesOneOrMoreExtensions,
safariHacksDisabled
} from "discourse/lib/utilities";
import { emojiUnescape } from "discourse/lib/text";
import { shortDate } from "discourse/lib/formatter";
@ -133,10 +134,11 @@ export default Ember.Controller.extend({
@computed(
"model.replyingToTopic",
"model.creatingPrivateMessage",
"model.targetUsernames"
"model.targetUsernames",
"model.composeState"
)
focusTarget(replyingToTopic, creatingPM, usernames) {
if (this.capabilities.isIOS) {
focusTarget(replyingToTopic, creatingPM, usernames, composeState) {
if (this.capabilities.isIOS && !safariHacksDisabled()) {
return "none";
}
@ -153,6 +155,10 @@ export default Ember.Controller.extend({
return "reply";
}
if (composeState === Composer.FULLSCREEN) {
return "editor";
}
return "title";
},
@ -542,18 +548,19 @@ export default Ember.Controller.extend({
) {
groups.forEach(group => {
let body;
const groupLink = Discourse.getURL(`/g/${group.name}/members`);
if (group.max_mentions < group.user_count) {
body = I18n.t("composer.group_mentioned_limit", {
group: "@" + group.name,
max: group.max_mentions,
group_link: Discourse.getURL(`/groups/${group.name}/members`)
group_link: groupLink
});
} else {
body = I18n.t("composer.group_mentioned", {
group: "@" + group.name,
count: group.user_count,
group_link: Discourse.getURL(`/groups/${group.name}/members`)
group_link: groupLink
});
}
@ -783,7 +790,11 @@ export default Ember.Controller.extend({
});
// Scope the categories drop down to the category we opened the composer with.
if (opts.categoryId && opts.draftKey !== "reply_as_new_topic") {
if (
opts.categoryId &&
opts.draftKey !== "reply_as_new_topic" &&
!opts.disableScopedCategory
) {
const category = this.site.categories.findBy("id", opts.categoryId);
if (category) {
this.set("scopedCategoryId", opts.categoryId);

View File

@ -106,7 +106,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
data: {
login: this.get("loginName"),
password: this.get("loginPassword"),
second_factor_token: this.get("loginSecondFactor"),
second_factor_token: this.get("secondFactorToken"),
second_factor_method: this.get("secondFactorMethod")
}
}).then(

View File

@ -66,10 +66,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
});
const isPrivateMessage = this.get("model.isPrivateMessage");
const canSplitTopic = this.get("canSplitTopic");
if (isPrivateMessage) {
this.set("selection", canSplitTopic ? "new_message" : "existing_message");
} else if (!canSplitTopic) {
this.set(
"selection",
this.get("canSplitToPM") ? "new_message" : "existing_message"
);
} else if (!this.get("canSplitTopic")) {
this.set("selection", "existing_topic");
Ember.run.next(() => $("#choose-topic-title").focus());
}
@ -85,6 +87,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
);
},
@computed("canSplitTopic")
canSplitToPM(canSplitTopic) {
return canSplitTopic && (this.currentUser && this.currentUser.admin);
},
actions: {
performMove() {
this.get("moveTypes").forEach(type => {

View File

@ -9,7 +9,7 @@ export default Ember.Controller.extend(PasswordValidation, {
isDeveloper: Ember.computed.alias("model.is_developer"),
admin: Ember.computed.alias("model.admin"),
secondFactorRequired: Ember.computed.alias("model.second_factor_required"),
backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
backupEnabled: Ember.computed.alias("model.backup_enabled"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
passwordRequired: true,
errorMessage: null,
@ -38,7 +38,7 @@ export default Ember.Controller.extend(PasswordValidation, {
type: "PUT",
data: {
password: this.get("accountPassword"),
second_factor_token: this.get("secondFactor"),
second_factor_token: this.get("secondFactorToken"),
second_factor_method: this.get("secondFactorMethod")
}
})

View File

@ -92,9 +92,9 @@ export default Ember.Controller.extend(
return userId !== this.get("currentUser.id");
},
@computed("model.second_factor_enabled")
canUpdateAssociatedAccounts(secondFactorEnabled) {
if (secondFactorEnabled) {
@computed("model.second_factor_enabled", "canCheckEmails")
canUpdateAssociatedAccounts(secondFactorEnabled, canCheckEmails) {
if (secondFactorEnabled || !canCheckEmails) {
return false;
}
@ -106,9 +106,15 @@ export default Ember.Controller.extend(
@computed("showAllAuthTokens", "model.user_auth_tokens")
authTokens(showAllAuthTokens, tokens) {
tokens.sort((a, b) =>
a.is_active ? -1 : b.is_active ? 1 : b.seen_at.localeCompare(a.seen_at)
);
tokens.sort((a, b) => {
if (a.is_active) {
return -1;
} else if (b.is_active) {
return 1;
} else {
return b.seen_at.localeCompare(a.seen_at);
}
});
return showAllAuthTokens
? tokens

View File

@ -20,12 +20,12 @@ export default Ember.Controller.extend(PreferencesTabController, {
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
},
canSave: function() {
return (
this.get("currentUser.id") === this.get("model.id") ||
this.get("currentUser.admin")
);
}.property(),
@computed
canSee() {
return this.get("currentUser.id") === this.get("model.id");
},
canSave: Ember.computed.or("canSee", "currentUser.admin"),
actions: {
save() {

View File

@ -5,12 +5,12 @@ import {
observes
} from "ember-addons/ember-computed-decorators";
import {
currentThemeId,
listThemes,
previewTheme,
setLocalTheme
} from "discourse/lib/theme-selector";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { safariHacksDisabled, isiPad } from "discourse/lib/utilities";
const USER_HOMES = {
1: "latest",
@ -46,19 +46,22 @@ export default Ember.Controller.extend(PreferencesTabController, {
},
preferencesController: Ember.inject.controller("preferences"),
makeThemeDefault: true,
makeTextSizeDefault: true,
@computed()
isiPad() {
return isiPad();
},
@computed()
disableSafariHacks() {
return safariHacksDisabled();
},
@computed()
availableLocales() {
return JSON.parse(this.siteSettings.available_locales);
},
@computed()
themeId() {
return currentThemeId();
},
@computed
textSizes() {
return TEXT_SIZES.map(value => {
@ -81,6 +84,16 @@ export default Ember.Controller.extend(PreferencesTabController, {
previewTheme([id]);
},
@computed("model.user_option.theme_ids", "themeId")
showThemeSetDefault(userOptionThemes, selectedTheme) {
return !userOptionThemes || userOptionThemes[0] !== selectedTheme;
},
@computed("model.user_option.text_size", "textSize")
showTextSetDefault(userOptionTextSize, selectedTextSize) {
return userOptionTextSize !== selectedTextSize;
},
homeChanged() {
const siteHome = this.siteSettings.top_menu.split("|")[0].split(",")[0];
const userHome = USER_HOMES[this.get("model.user_option.homepage_id")];
@ -120,17 +133,31 @@ export default Ember.Controller.extend(PreferencesTabController, {
.then(() => {
this.set("saved", true);
if (!makeThemeDefault) {
if (makeThemeDefault) {
setLocalTheme([]);
} else {
setLocalTheme(
[this.get("themeId")],
this.get("model.user_option.theme_key_seq")
);
}
if (!makeTextSizeDefault) {
if (makeTextSizeDefault) {
this.get("model").updateTextSizeCookie(null);
} else {
this.get("model").updateTextSizeCookie(this.get("textSize"));
}
this.homeChanged();
if (this.get("isiPad")) {
if (safariHacksDisabled() !== this.get("disableSafariHacks")) {
Discourse.set("assetVersion", "forceRefresh");
}
localStorage.setItem(
"safari-hacks-disabled",
this.get("disableSafariHacks").toString()
);
}
})
.catch(popupAjaxError);
},

View File

@ -1,6 +1,7 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { default as DiscourseURL, userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
export default Ember.Controller.extend({
loading: false,
@ -11,10 +12,15 @@ export default Ember.Controller.extend({
"model.second_factor_remaining_backup_codes"
),
backupCodes: null,
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
@computed("secondFactorToken")
isValidSecondFactorToken(secondFactorToken) {
return secondFactorToken && secondFactorToken.length === 6;
@computed("secondFactorToken", "secondFactorMethod")
isValidSecondFactorToken(secondFactorToken, secondFactorMethod) {
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) {
return secondFactorToken && secondFactorToken.length === 6;
} else if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) {
return secondFactorToken && secondFactorToken.length === 16;
}
},
@computed("isValidSecondFactorToken", "backupEnabled", "loading")
@ -59,7 +65,12 @@ export default Ember.Controller.extend({
this.set("loading", true);
this.get("model")
.toggleSecondFactor(this.get("secondFactorToken"), false, 2)
.toggleSecondFactor(
this.get("secondFactorToken"),
this.get("secondFactorMethod"),
SECOND_FACTOR_METHODS.BACKUP_CODE,
false
)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);
@ -79,7 +90,10 @@ export default Ember.Controller.extend({
if (!this.get("secondFactorToken")) return;
this.set("loading", true);
this.get("model")
.generateSecondFactorCodes(this.get("secondFactorToken"))
.generateSecondFactorCodes(
this.get("secondFactorToken"),
this.get("secondFactorMethod")
)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);

View File

@ -2,6 +2,7 @@ import { default as computed } from "ember-addons/ember-computed-decorators";
import { default as DiscourseURL, userPath } from "discourse/lib/url";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { findAll } from "discourse/models/login-method";
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
export default Ember.Controller.extend({
loading: false,
@ -13,6 +14,8 @@ export default Ember.Controller.extend({
showSecondFactorKey: false,
errorMessage: null,
newUsername: null,
backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
loaded: Ember.computed.and("secondFactorImage", "secondFactorKey"),
@ -41,7 +44,12 @@ export default Ember.Controller.extend({
this.set("loading", true);
this.get("model")
.toggleSecondFactor(this.get("secondFactorToken"), enable, 1)
.toggleSecondFactor(
this.get("secondFactorToken"),
this.get("secondFactorMethod"),
SECOND_FACTOR_METHODS.TOTP,
enable
)
.then(response => {
if (response.error) {
this.set("errorMessage", response.error);

View File

@ -1,4 +1,7 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import {
default as NavItem,
@ -19,14 +22,14 @@ if (extraNavItemProperties) {
if (customNavItemHref) {
customNavItemHref(function(navItem) {
if (navItem.get("tagId")) {
var name = navItem.get("name");
const name = navItem.get("name");
if (!Discourse.Site.currentProp("filters").includes(name)) {
return null;
}
var path = "/tags/",
category = navItem.get("category");
let path = "/tags/";
const category = navItem.get("category");
if (category) {
path += "c/";
@ -37,8 +40,8 @@ if (customNavItemHref) {
path += "/";
}
path += navItem.get("tagId") + "/l/";
return path + name.replace(" ", "-");
path += `${navItem.get("tagId")}/l/`;
return `${path}${name.replace(" ", "-")}`;
} else {
return null;
}
@ -66,9 +69,10 @@ export default Ember.Controller.extend(BulkTopicSelection, {
categories: Ember.computed.alias("site.categoriesList"),
createTopicLabel: function() {
return this.get("list.draft") ? "topic.open_draft" : "topic.create";
}.property("list", "list.draft"),
@computed("list", "list.draft")
createTopicLabel(list, listDraft) {
return listDraft ? "topic.open_draft" : "topic.create";
},
@computed("canCreateTopic", "category", "canCreateTopicOnCategory")
createTopicDisabled(canCreateTopic, category, canCreateTopicOnCategory) {
@ -85,92 +89,87 @@ export default Ember.Controller.extend(BulkTopicSelection, {
"q"
],
navItems: function() {
return NavItem.buildList(this.get("category"), {
tagId: this.get("tag.id"),
filterMode: this.get("filterMode")
@computed("category", "tag.id", "filterMode")
navItems(category, tagId, filterMode) {
return NavItem.buildList(category, {
tagId,
filterMode
});
}.property("category", "tag.id", "filterMode"),
},
showTagFilter: function() {
@computed("category")
showTagFilter() {
return Discourse.SiteSettings.show_filter_by_tag;
}.property("category"),
},
showAdminControls: function() {
return (
!this.get("additionalTags") &&
this.get("canAdminTag") &&
!this.get("category")
);
}.property("additionalTags", "canAdminTag", "category"),
@computed("additionalTags", "canAdminTag", "category")
showAdminControls(additionalTags, canAdminTag, category) {
return !additionalTags && canAdminTag && !category;
},
loadMoreTopics() {
return this.get("list").loadMore();
},
_showFooter: function() {
@observes("list.canLoadMore")
_showFooter() {
this.set("application.showFooter", !this.get("list.canLoadMore"));
}.observes("list.canLoadMore"),
},
footerMessage: function() {
if (this.get("loading") || this.get("list.topics.length") !== 0) {
@computed("navMode", "list.topics.length", "loading")
footerMessage(navMode, listTopicsLength, loading) {
if (loading || listTopicsLength !== 0) {
return;
}
if (this.get("list.topics.length") === 0) {
return I18n.t("tagging.topics.none." + this.get("navMode"), {
if (listTopicsLength === 0) {
return I18n.t(`tagging.topics.none.${navMode}`, {
tag: this.get("tag.id")
});
} else {
return I18n.t("tagging.topics.bottom." + this.get("navMode"), {
return I18n.t(`tagging.topics.bottom.${navMode}`, {
tag: this.get("tag.id")
});
}
}.property("navMode", "list.topics.length", "loading"),
},
actions: {
changeSort(sortBy) {
if (sortBy === this.get("order")) {
changeSort(order) {
if (order === this.get("order")) {
this.toggleProperty("ascending");
} else {
this.setProperties({ order: sortBy, ascending: false });
this.setProperties({ order, ascending: false });
}
this.send("invalidateModel");
},
refresh() {
const self = this;
// TODO: this probably doesn't work anymore
return this.store
.findFiltered("topicList", { filter: "tags/" + this.get("tag.id") })
.then(function(list) {
self.set("list", list);
self.resetSelected();
.then(list => {
this.set("list", list);
this.resetSelected();
});
},
deleteTag() {
const self = this;
const numTopics =
this.get("list.topic_list.tags.firstObject.topic_count") || 0;
const confirmText =
numTopics === 0
? I18n.t("tagging.delete_confirm_no_topics")
: I18n.t("tagging.delete_confirm", { count: numTopics });
bootbox.confirm(confirmText, function(result) {
if (!result) {
return;
}
self
.get("tag")
bootbox.confirm(confirmText, result => {
if (!result) return;
this.get("tag")
.destroyRecord()
.then(function() {
self.transitionToRoute("tags.index");
})
.catch(function() {
bootbox.alert(I18n.t("generic_error"));
});
.then(() => this.transitionToRoute("tags.index"))
.catch(() => bootbox.alert(I18n.t("generic_error")));
});
},

View File

@ -1,8 +1,11 @@
import Invite from "discourse/models/invite";
import debounce from "discourse/lib/debounce";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
default as computed,
observes
} from "ember-addons/ember-computed-decorators";
// This controller handles actions related to a user's invitations
export default Ember.Controller.extend({
user: null,
model: null,
@ -13,84 +16,67 @@ export default Ember.Controller.extend({
invitesLoading: false,
reinvitedAll: false,
rescindedAll: false,
searchTerm: null,
init: function() {
init() {
this._super(...arguments);
this.set("searchTerm", "");
},
/**
Observe the search term box with a debouncer and change the results.
@observes searchTerm
**/
@observes("searchTearm")
_searchTermChanged: debounce(function() {
var self = this;
Invite.findInvitedBy(
self.get("user"),
this.get("user"),
this.get("filter"),
this.get("searchTerm")
).then(function(invites) {
self.set("model", invites);
});
}, 250).observes("searchTerm"),
).then(invites => this.set("model", invites));
}, 250),
inviteRedeemed: Ember.computed.equal("filter", "redeemed"),
showBulkActionButtons: function() {
@computed("filter")
showBulkActionButtons(filter) {
return (
this.get("filter") === "pending" &&
filter === "pending" &&
this.get("model").invites.length > 4 &&
this.currentUser.get("staff")
);
}.property("filter"),
},
/**
Can the currently logged in user invite users to the site
@property canInviteToForum
**/
canInviteToForum: function() {
@computed
canInviteToForum() {
return Discourse.User.currentProp("can_invite_to_forum");
}.property(),
},
/**
Can the currently logged in user bulk invite users to the site (only Admin is allowed to perform this operation)
@property canBulkInvite
**/
canBulkInvite: function() {
@computed
canBulkInvite() {
return Discourse.User.currentProp("admin");
}.property(),
},
/**
Should the search filter input box be displayed?
showSearch: Ember.computed.gte("totalInvites", 10),
@property showSearch
**/
showSearch: function() {
return this.get("totalInvites") > 9;
}.property("totalInvites"),
pendingLabel: function() {
if (this.get("invitesCount.total") > 50) {
@computed("invitesCount.total", "invitesCount.pending}")
pendingLabel(invitesCountTotal, invitesCountPending) {
if (invitesCountTotal > 50) {
return I18n.t("user.invited.pending_tab_with_count", {
count: this.get("invitesCount.pending")
count: invitesCountPending
});
} else {
return I18n.t("user.invited.pending_tab");
}
}.property("invitesCount"),
},
redeemedLabel: function() {
if (this.get("invitesCount.total") > 50) {
@computed("invitesCount.total", "invitesCount.redeemed")
redeemedLabel(invitesCountTotal, invitesCountRedeemed) {
if (invitesCountTotal > 50) {
return I18n.t("user.invited.redeemed_tab_with_count", {
count: this.get("invitesCount.redeemed")
count: invitesCountRedeemed
});
} else {
return I18n.t("user.invited.redeemed_tab");
}
}.property("invitesCount"),
},
actions: {
rescind(invite) {
@ -120,34 +106,31 @@ export default Ember.Controller.extend({
bootbox.confirm(I18n.t("user.invited.reinvite_all_confirm"), confirm => {
if (confirm) {
Invite.reinviteAll()
.then(() => {
this.set("reinvitedAll", true);
})
.then(() => this.set("reinvitedAll", true))
.catch(popupAjaxError);
}
});
},
loadMore() {
var self = this;
var model = self.get("model");
const model = this.get("model");
if (self.get("canLoadMore") && !self.get("invitesLoading")) {
self.set("invitesLoading", true);
if (this.get("canLoadMore") && !this.get("invitesLoading")) {
this.set("invitesLoading", true);
Invite.findInvitedBy(
self.get("user"),
self.get("filter"),
self.get("searchTerm"),
this.get("user"),
this.get("filter"),
this.get("searchTerm"),
model.invites.length
).then(function(invite_model) {
self.set("invitesLoading", false);
).then(invite_model => {
this.set("invitesLoading", false);
model.invites.pushObjects(invite_model.invites);
if (
invite_model.invites.length === 0 ||
invite_model.invites.length <
Discourse.SiteSettings.invites_per_page
) {
self.set("canLoadMore", false);
this.set("canLoadMore", false);
}
});
}

View File

@ -145,6 +145,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
adminDelete() {
this.get("adminTools").deleteUser(this.get("model.id"));
},
ignoreUser() {
const user = this.get("model");
user.ignore().then(() => user.set("ignored", true));
},
watchUser() {
const user = this.get("model");
user.watch().then(() => user.set("ignored", false));
}
}
});

Some files were not shown because too many files have changed in this diff Show More