Version bump
This commit is contained in:
commit
a5df8c8dcf
@ -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/
|
||||
|
||||
20
Dangerfile
20
Dangerfile
@ -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
|
||||
|
||||
6
Gemfile
6
Gemfile
@ -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
|
||||
|
||||
26
Gemfile.lock
26
Gemfile.lock
@ -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
|
||||
|
||||
24
README.md
24
README.md
@ -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) — Our back end API is a Rails app. It responds to requests RESTfully in JSON.
|
||||
- [Ember.js](https://github.com/emberjs/ember.js) — Our front end is an Ember.js app that communicates with the Rails API.
|
||||
- [PostgreSQL](http://www.postgresql.org/) — Our main data store is in Postgres.
|
||||
- [Redis](http://redis.io/) — We use Redis as a cache and for transient data.
|
||||
- [PostgreSQL](https://www.postgresql.org/) — Our main data store is in Postgres.
|
||||
- [Redis](https://redis.io/) — 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 – 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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["install-theme-item"]
|
||||
});
|
||||
@ -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")) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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}")
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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}")
|
||||
});
|
||||
|
||||
@ -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}")
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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", {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.[]");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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") }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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"}}
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'}}
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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|}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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}}
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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")
|
||||
});
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 ``
|
||||
// match 1 ``
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 } : {};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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")
|
||||
@ -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"]
|
||||
});
|
||||
17
app/assets/javascripts/discourse/components/modal-tab.js.es6
Normal file
17
app/assets/javascripts/discourse/components/modal-tab.js.es6
Normal 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"));
|
||||
}
|
||||
});
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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")
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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")
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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")));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
Reference in New Issue
Block a user