Version bump
This commit is contained in:
commit
df61b9309d
2
.gitignore
vendored
2
.gitignore
vendored
@ -122,3 +122,5 @@ vendor/bundle/*
|
||||
# ignore nodejs files
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
|
||||
/vendor/data/GeoLite2-City.mmdb
|
||||
|
||||
@ -19,5 +19,27 @@ PreCommit:
|
||||
command: ['bundle', 'exec', 'rubocop']
|
||||
EsLint:
|
||||
enabled: true
|
||||
command: ['eslint', '--ext', '.es6', '-f', 'compact']
|
||||
required_executable: './node_modules/.bin/eslint'
|
||||
install_command: 'yarn install'
|
||||
command: ['yarn', 'eslint', '--ext', '.es6', '-f', 'compact']
|
||||
include: '**/*.es6'
|
||||
YamlSyntax:
|
||||
enabled: true
|
||||
|
||||
PostCheckout:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
|
||||
PostMerge:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
|
||||
PostRewrite:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
|
||||
@ -84,6 +84,7 @@ script:
|
||||
else
|
||||
if [ '$QUNIT_RUN' == '1' ]; then
|
||||
bundle exec rake qunit:test['500000'] && \
|
||||
bundle exec rake qunit:test['500000','/wizard/qunit'] && \
|
||||
bundle exec rake plugin:qunit
|
||||
else
|
||||
bundle exec rspec && bundle exec rake plugin:spec
|
||||
|
||||
@ -2,8 +2,9 @@ if github.pr_json && (github.pr_json["additions"] || 0) > 250 || (github.pr_json
|
||||
warn("This pull request is big! We prefer smaller PRs whenever possible, as they are easier to review. Can this be split into a few smaller PRs?")
|
||||
end
|
||||
|
||||
prettier_offenses = `prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split('\n')
|
||||
if !prettier_offenses.empty?
|
||||
prettier_offenses = `yarn --silent prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split("\n")
|
||||
|
||||
unless prettier_offenses.empty?
|
||||
fail(%{
|
||||
This PR doesn't match our required code formatting standards, as enforced by prettier.io. <a href='https://meta.discourse.org/t/prettier-code-formatting-tool/93212'>Here's how to set up prettier in your code editor.</a>\n
|
||||
#{prettier_offenses.map { |o| github.html_link(o) }.join("\n")}
|
||||
|
||||
3
Gemfile
3
Gemfile
@ -34,7 +34,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.60'
|
||||
gem 'onebox', '1.8.63'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -194,3 +194,4 @@ end
|
||||
|
||||
gem 'webpush', require: false
|
||||
gem 'colored2', require: false
|
||||
gem 'maxminddb'
|
||||
|
||||
28
Gemfile.lock
28
Gemfile.lock
@ -44,20 +44,20 @@ GEM
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
aws-eventstream (1.0.1)
|
||||
aws-partitions (1.92.0)
|
||||
aws-sdk-core (3.21.2)
|
||||
aws-partitions (1.104.0)
|
||||
aws-sdk-core (3.27.0)
|
||||
aws-eventstream (~> 1.0)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.5.0)
|
||||
aws-sdk-core (~> 3)
|
||||
aws-sdk-kms (1.9.0)
|
||||
aws-sdk-core (~> 3, >= 3.26.0)
|
||||
aws-sigv4 (~> 1.0)
|
||||
aws-sdk-s3 (1.14.0)
|
||||
aws-sdk-core (~> 3, >= 3.21.2)
|
||||
aws-sdk-s3 (1.19.0)
|
||||
aws-sdk-core (~> 3, >= 3.26.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.0)
|
||||
aws-sigv4 (1.0.2)
|
||||
aws-sigv4 (1.0.3)
|
||||
barber (0.12.0)
|
||||
ember-source (>= 1.0, < 3.1)
|
||||
execjs (>= 1.2, < 3)
|
||||
@ -191,14 +191,15 @@ GEM
|
||||
lru_redux (1.1.0)
|
||||
mail (2.7.1.rc1)
|
||||
mini_mime (>= 0.1.1)
|
||||
memory_profiler (0.9.10)
|
||||
maxminddb (0.1.21)
|
||||
memory_profiler (0.9.12)
|
||||
message_bus (2.1.5)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mini_mime (1.0.0)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_racer (0.2.0)
|
||||
mini_racer (0.2.3)
|
||||
libv8 (>= 6.3)
|
||||
mini_scheduler (0.8.1)
|
||||
mini_sql (0.1.10)
|
||||
@ -216,7 +217,7 @@ GEM
|
||||
mustache (1.0.5)
|
||||
nap (1.1.0)
|
||||
no_proxy_fix (0.1.2)
|
||||
nokogiri (1.8.3)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogumbo (1.5.0)
|
||||
nokogiri
|
||||
@ -257,7 +258,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.60)
|
||||
onebox (1.8.63)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -488,6 +489,7 @@ DEPENDENCIES
|
||||
logster
|
||||
lru_redux
|
||||
mail (= 2.7.1.rc1)
|
||||
maxminddb
|
||||
memory_profiler
|
||||
message_bus
|
||||
mini_mime
|
||||
@ -510,7 +512,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.60)
|
||||
onebox (= 1.8.63)
|
||||
openid-redis-store
|
||||
pg
|
||||
pry-nav
|
||||
@ -555,4 +557,4 @@ DEPENDENCIES
|
||||
webpush
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.4
|
||||
1.16.6
|
||||
|
||||
22
README.md
22
README.md
@ -22,11 +22,15 @@ Browse [lots more notable Discourse instances](https://www.discourse.org/custome
|
||||
|
||||
## Development
|
||||
|
||||
1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/).
|
||||
To get your environment setup, follow the community setup guide for your operating system.
|
||||
|
||||
2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md).
|
||||
1. If you're on macOS, try the [macOS development guide](https://meta.discourse.org/t/beginners-guide-to-install-discourse-on-macos-for-development/15772).
|
||||
1. If you're on Ubuntu, try the [Ubuntu development guide](https://meta.discourse.org/t/beginners-guide-to-install-discourse-on-ubuntu-for-development/14727).
|
||||
1. If you're on Windows, try the [Windows 10 development guide](https://meta.discourse.org/t/beginners-guide-to-install-discourse-on-windows-10-for-development/75149).
|
||||
|
||||
Before you get started, ensure you have the following minimum versions: [Ruby 2.4+](http://www.ruby-lang.org/en/downloads/), [PostgreSQL 9.3+](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!
|
||||
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.4+](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!
|
||||
|
||||
## Setting up Discourse
|
||||
|
||||
@ -38,12 +42,12 @@ If you're looking for business class hosting, see [discourse.org/buy](https://ww
|
||||
|
||||
Discourse is built for the *next* 10 years of the Internet, so our requirements are high:
|
||||
|
||||
| Browsers | Tablets | Phones |
|
||||
| -------- | ------- | ----------- |
|
||||
| Safari 6.1+ | iPad 3+ | iOS 8+ |
|
||||
| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
|
||||
| Internet Explorer 11+ | | |
|
||||
| Firefox 27+ | | |
|
||||
| Browsers | Tablets | Phones |
|
||||
| --------------------- | ------------ | ------------ |
|
||||
| Safari 6.1+ | iPad 3+ | iOS 8+ |
|
||||
| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
|
||||
| Internet Explorer 11+ | | |
|
||||
| Firefox 27+ | | |
|
||||
|
||||
## Built With
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 716 B |
@ -1,8 +1,6 @@
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const LOAD_ASYNC = !Ember.testing;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
mode: "css",
|
||||
classNames: ["ace-wrapper"],
|
||||
@ -26,7 +24,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@observes("mode")
|
||||
modeChanged() {
|
||||
if (LOAD_ASYNC && this._editor && !this._skipContentChangeEvent) {
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setMode("ace/mode/" + this.get("mode"));
|
||||
}
|
||||
},
|
||||
@ -71,21 +69,17 @@ export default Ember.Component.extend({
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(() => {
|
||||
loadScript("/javascripts/ace/ace.js").then(() => {
|
||||
window.ace.require(["ace/ace"], loadedAce => {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const editor = loadedAce.edit(this.$(".ace")[0]);
|
||||
|
||||
if (LOAD_ASYNC) {
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
}
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setOptions({ fontSize: "14px" });
|
||||
if (LOAD_ASYNC) {
|
||||
editor.getSession().setMode("ace/mode/" + this.get("mode"));
|
||||
}
|
||||
editor.getSession().setMode("ace/mode/" + this.get("mode"));
|
||||
editor.on("change", () => {
|
||||
this._skipContentChangeEvent = true;
|
||||
this.set("content", editor.getSession().getValue());
|
||||
|
||||
@ -2,6 +2,7 @@ import debounce from "discourse/lib/debounce";
|
||||
import { renderSpinner } from "discourse/helpers/loading-spinner";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { bufferedRender } from "discourse-common/lib/buffered-render";
|
||||
import { observes, on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend(
|
||||
bufferedRender({
|
||||
@ -21,30 +22,38 @@ export default Ember.Component.extend(
|
||||
$div.scrollTop = $div.scrollHeight;
|
||||
},
|
||||
|
||||
_updateFormattedLogs: debounce(function() {
|
||||
const logs = this.get("logs");
|
||||
if (logs.length === 0) {
|
||||
@on("init")
|
||||
@observes("logs.[]")
|
||||
_resetFormattedLogs() {
|
||||
if (this.get("logs").length === 0) {
|
||||
this._reset(); // reset the cached logs whenever the model is reset
|
||||
} else {
|
||||
// do the log formatting only once for HELLish performance
|
||||
let formattedLogs = this.get("formattedLogs");
|
||||
for (let i = this.get("index"), length = logs.length; i < length; i++) {
|
||||
const date = logs[i].get("timestamp"),
|
||||
message = escapeExpression(logs[i].get("message"));
|
||||
formattedLogs += "[" + date + "] " + message + "\n";
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
this.setProperties({
|
||||
formattedLogs: formattedLogs,
|
||||
index: logs.length
|
||||
});
|
||||
// force rerender
|
||||
this.rerenderBuffer();
|
||||
}
|
||||
},
|
||||
|
||||
@on("init")
|
||||
@observes("logs.[]")
|
||||
_updateFormattedLogs: debounce(function() {
|
||||
const logs = this.get("logs");
|
||||
if (logs.length === 0) return;
|
||||
|
||||
// do the log formatting only once for HELLish performance
|
||||
let formattedLogs = this.get("formattedLogs");
|
||||
for (let i = this.get("index"), length = logs.length; i < length; i++) {
|
||||
const date = logs[i].get("timestamp"),
|
||||
message = escapeExpression(logs[i].get("message"));
|
||||
formattedLogs += "[" + date + "] " + message + "\n";
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
this.setProperties({
|
||||
formattedLogs: formattedLogs,
|
||||
index: logs.length
|
||||
});
|
||||
// force rerender
|
||||
this.rerenderBuffer();
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", this, this._scrollDown);
|
||||
}, 150)
|
||||
.observes("logs.[]")
|
||||
.on("init"),
|
||||
}, 150),
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const formattedLogs = this.get("formattedLogs");
|
||||
|
||||
136
app/assets/javascripts/admin/components/themes-list-item.js.es6
Normal file
136
app/assets/javascripts/admin/components/themes-list-item.js.es6
Normal file
@ -0,0 +1,136 @@
|
||||
import {
|
||||
default as computed,
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const MAX_COMPONENTS = 4;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
childrenExpanded: false,
|
||||
classNames: ["themes-list-item"],
|
||||
classNameBindings: ["theme.selected:selected"],
|
||||
hasComponents: Em.computed.gt("children.length", 0),
|
||||
displayComponents: Em.computed.and("hasComponents", "theme.isActive"),
|
||||
displayHasMore: Em.computed.gt("theme.childThemes.length", MAX_COMPONENTS),
|
||||
|
||||
click(e) {
|
||||
if (!$(e.target).hasClass("others-count")) {
|
||||
this.navigateToTheme();
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.scheduleAnimation();
|
||||
},
|
||||
|
||||
@observes("theme.selected")
|
||||
triggerAnimation() {
|
||||
this.animate();
|
||||
},
|
||||
|
||||
scheduleAnimation() {
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
this.animate(true);
|
||||
});
|
||||
},
|
||||
|
||||
animate(isInitial) {
|
||||
const $container = this.$();
|
||||
const $list = this.$(".components-list");
|
||||
if ($list.length === 0 || Ember.testing) {
|
||||
return;
|
||||
}
|
||||
const duration = 300;
|
||||
if (this.get("theme.selected")) {
|
||||
this.collapseComponentsList($container, $list, duration);
|
||||
} else if (!isInitial) {
|
||||
this.expandComponentsList($container, $list, duration);
|
||||
}
|
||||
},
|
||||
|
||||
@computed(
|
||||
"theme.component",
|
||||
"theme.childThemes.@each.name",
|
||||
"theme.childThemes.length",
|
||||
"childrenExpanded"
|
||||
)
|
||||
children() {
|
||||
const theme = this.get("theme");
|
||||
let children = theme.get("childThemes");
|
||||
if (theme.get("component") || !children) {
|
||||
return [];
|
||||
}
|
||||
children = this.get("childrenExpanded")
|
||||
? children
|
||||
: children.slice(0, MAX_COMPONENTS);
|
||||
return children.map(t => t.get("name"));
|
||||
},
|
||||
|
||||
@computed(
|
||||
"theme.childThemes.length",
|
||||
"theme.component",
|
||||
"childrenExpanded",
|
||||
"children.length"
|
||||
)
|
||||
moreCount(childrenCount, component, expanded) {
|
||||
if (component || !childrenCount || expanded) {
|
||||
return 0;
|
||||
}
|
||||
return childrenCount - MAX_COMPONENTS;
|
||||
},
|
||||
|
||||
expandComponentsList($container, $list, duration) {
|
||||
$container.css("height", `${$container.height()}px`);
|
||||
$list.css("display", "");
|
||||
$container.animate(
|
||||
{
|
||||
height: `${$container.height() + $list.outerHeight(true)}px`
|
||||
},
|
||||
{
|
||||
duration,
|
||||
done: () => {
|
||||
$list.css("display", "");
|
||||
$container.css("height", "");
|
||||
}
|
||||
}
|
||||
);
|
||||
$list.animate(
|
||||
{
|
||||
opacity: 1
|
||||
},
|
||||
{
|
||||
duration
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
collapseComponentsList($container, $list, duration) {
|
||||
$container.animate(
|
||||
{
|
||||
height: `${$container.height() - $list.outerHeight(true)}px`
|
||||
},
|
||||
{
|
||||
duration,
|
||||
done: () => {
|
||||
$list.css("display", "none");
|
||||
$container.css("height", "");
|
||||
}
|
||||
}
|
||||
);
|
||||
$list.animate(
|
||||
{
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
duration
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleChildrenExpanded() {
|
||||
this.toggleProperty("childrenExpanded");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,87 @@
|
||||
import { THEMES, COMPONENTS } from "admin/models/theme";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
THEMES: THEMES,
|
||||
COMPONENTS: COMPONENTS,
|
||||
|
||||
classNames: ["themes-list"],
|
||||
hasThemes: Ember.computed.gt("themes.length", 0)
|
||||
|
||||
hasThemes: Em.computed.gt("themesList.length", 0),
|
||||
hasUserThemes: Em.computed.gt("userThemes.length", 0),
|
||||
hasInactiveThemes: Em.computed.gt("inactiveThemes.length", 0),
|
||||
|
||||
themesTabActive: Em.computed.equal("currentTab", THEMES),
|
||||
componentsTabActive: Em.computed.equal("currentTab", COMPONENTS),
|
||||
|
||||
@computed("themes", "components", "currentTab")
|
||||
themesList(themes, components) {
|
||||
if (this.get("themesTabActive")) {
|
||||
return themes;
|
||||
} else {
|
||||
return components;
|
||||
}
|
||||
},
|
||||
|
||||
@computed(
|
||||
"themesList",
|
||||
"currentTab",
|
||||
"themesList.@each.user_selectable",
|
||||
"themesList.@each.default"
|
||||
)
|
||||
inactiveThemes(themes) {
|
||||
if (this.get("componentsTabActive")) {
|
||||
return [];
|
||||
}
|
||||
return themes.filter(
|
||||
theme => !theme.get("user_selectable") && !theme.get("default")
|
||||
);
|
||||
},
|
||||
|
||||
@computed(
|
||||
"themesList",
|
||||
"currentTab",
|
||||
"themesList.@each.user_selectable",
|
||||
"themesList.@each.default"
|
||||
)
|
||||
userThemes(themes) {
|
||||
if (this.get("componentsTabActive")) {
|
||||
return [];
|
||||
}
|
||||
themes = themes.filter(
|
||||
theme => theme.get("user_selectable") || theme.get("default")
|
||||
);
|
||||
return _.sortBy(themes, t => {
|
||||
return [
|
||||
!t.get("default"),
|
||||
!t.get("user_selectable"),
|
||||
t.get("name").toLowerCase()
|
||||
];
|
||||
});
|
||||
},
|
||||
|
||||
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")) {
|
||||
this.set("currentTab", newTab);
|
||||
}
|
||||
},
|
||||
navigateToTheme(theme) {
|
||||
Em.getOwner(this)
|
||||
.lookup("router:main")
|
||||
.transitionTo("adminCustomizeThemes.show", theme);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,8 +5,10 @@ import {
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
maximized: false,
|
||||
section: null,
|
||||
currentTarget: 0,
|
||||
maximized: false,
|
||||
previewUrl: url("model.id", "/admin/themes/%@/preview"),
|
||||
|
||||
editRouteName: "adminCustomizeThemes.edit",
|
||||
|
||||
@ -86,8 +88,6 @@ export default Ember.Controller.extend({
|
||||
return this.get("model").hasEdited(target);
|
||||
},
|
||||
|
||||
currentTarget: 0,
|
||||
|
||||
setTargetName: function(name) {
|
||||
const target = this.get("targets").find(t => t.name === name);
|
||||
this.set("currentTarget", target && target.id);
|
||||
@ -152,21 +152,20 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
previewUrl: url("model.id", "/admin/themes/%@/preview"),
|
||||
@computed("maximized")
|
||||
maximizeIcon(maximized) {
|
||||
return maximized ? "compress" : "expand";
|
||||
},
|
||||
|
||||
maximizeIcon: function() {
|
||||
return this.get("maximized") ? "compress" : "expand";
|
||||
}.property("maximized"),
|
||||
@computed("model.isSaving")
|
||||
saveButtonText(isSaving) {
|
||||
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
|
||||
},
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get("model.isSaving")
|
||||
? I18n.t("saving")
|
||||
: I18n.t("admin.customize.save");
|
||||
}.property("model.isSaving"),
|
||||
|
||||
saveDisabled: function() {
|
||||
return !this.get("model.changed") || this.get("model.isSaving");
|
||||
}.property("model.changed", "model.isSaving"),
|
||||
@computed("model.changed", "model.isSaving")
|
||||
saveDisabled(changed, isSaving) {
|
||||
return !changed || isSaving;
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
|
||||
@ -1,119 +1,88 @@
|
||||
import {
|
||||
default as computed,
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import ThemeSettings from "admin/models/theme-settings";
|
||||
import { THEMES, COMPONENTS } from "admin/models/theme";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const SETTINGS_TYPE_ID = 5;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
downloadUrl: url("model.id", "/admin/themes/%@"),
|
||||
previewUrl: url("model.id", "/admin/themes/%@/preview"),
|
||||
addButtonDisabled: Em.computed.empty("selectedChildThemeId"),
|
||||
editRouteName: "adminCustomizeThemes.edit",
|
||||
|
||||
@observes("allowChildThemes")
|
||||
setSelectedThemeId() {
|
||||
const available = this.get("selectableChildThemes");
|
||||
if (
|
||||
!this.get("selectedChildThemeId") &&
|
||||
available &&
|
||||
available.length > 0
|
||||
) {
|
||||
this.set("selectedChildThemeId", available[0].get("id"));
|
||||
}
|
||||
},
|
||||
|
||||
@computed("model", "allThemes", "model.component")
|
||||
parentThemes(model, allThemes) {
|
||||
if (!model.get("component")) {
|
||||
return null;
|
||||
}
|
||||
let parents = allThemes.filter(theme =>
|
||||
const parents = allThemes.filter(theme =>
|
||||
_.contains(theme.get("childThemes"), model)
|
||||
);
|
||||
return parents.length === 0 ? null : parents;
|
||||
},
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
hasEditedFields(fields) {
|
||||
return fields.any(
|
||||
f => !Em.isBlank(f.value) && f.type_id !== SETTINGS_TYPE_ID
|
||||
);
|
||||
},
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
editedDescriptions(fields) {
|
||||
let descriptions = [];
|
||||
let description = target => {
|
||||
let current = fields.filter(
|
||||
field => field.target === target && !Em.isBlank(field.value)
|
||||
);
|
||||
if (current.length > 0) {
|
||||
let text = I18n.t("admin.customize.theme." + target);
|
||||
let localized = current.map(f =>
|
||||
I18n.t("admin.customize.theme." + f.name + ".text")
|
||||
);
|
||||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
@computed("model.editedFields")
|
||||
editedFieldsFormatted() {
|
||||
const descriptions = [];
|
||||
["common", "desktop", "mobile"].forEach(target => {
|
||||
descriptions.push(description(target));
|
||||
const fields = this.editedFieldsForTarget(target);
|
||||
if (fields.length < 1) {
|
||||
return;
|
||||
}
|
||||
let resultString = I18n.t("admin.customize.theme." + target);
|
||||
const formattedFields = fields
|
||||
.map(f => I18n.t("admin.customize.theme." + f.name + ".text"))
|
||||
.join(" , ");
|
||||
resultString += `: ${formattedFields}`;
|
||||
descriptions.push(resultString);
|
||||
});
|
||||
return descriptions.reject(d => Em.isBlank(d));
|
||||
return descriptions;
|
||||
},
|
||||
|
||||
previewUrl: url("model.id", "/admin/themes/%@/preview"),
|
||||
|
||||
@computed("colorSchemeId", "model.color_scheme_id")
|
||||
colorSchemeChanged(colorSchemeId, existingId) {
|
||||
colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId);
|
||||
return colorSchemeId !== existingId;
|
||||
},
|
||||
|
||||
@computed(
|
||||
"availableChildThemes",
|
||||
"model.childThemes.@each",
|
||||
"model",
|
||||
"allowChildThemes"
|
||||
)
|
||||
selectableChildThemes(available, childThemes, allowChildThemes) {
|
||||
if (!allowChildThemes && (!childThemes || childThemes.length === 0)) {
|
||||
return null;
|
||||
@computed("availableChildThemes", "model.childThemes.@each", "model")
|
||||
selectableChildThemes(available, childThemes) {
|
||||
if (available) {
|
||||
const themes = !childThemes
|
||||
? available
|
||||
: available.filter(theme => childThemes.indexOf(theme) === -1);
|
||||
return themes.length === 0 ? null : themes;
|
||||
}
|
||||
|
||||
let themes = [];
|
||||
available.forEach(t => {
|
||||
if (!childThemes || childThemes.indexOf(t) === -1) {
|
||||
themes.push(t);
|
||||
}
|
||||
});
|
||||
return themes.length === 0 ? null : themes;
|
||||
},
|
||||
|
||||
@computed("allThemes", "allThemes.length", "model.component", "model")
|
||||
availableChildThemes(allThemes, count, component) {
|
||||
if (count === 1 || component) {
|
||||
return null;
|
||||
@computed("allThemes", "model.component", "model")
|
||||
availableChildThemes(allThemes) {
|
||||
if (!this.get("model.component")) {
|
||||
const themeId = this.get("model.id");
|
||||
return allThemes.filter(
|
||||
theme => theme.get("id") !== themeId && theme.get("component")
|
||||
);
|
||||
}
|
||||
|
||||
const themeId = this.get("model.id");
|
||||
|
||||
let themes = [];
|
||||
allThemes.forEach(theme => {
|
||||
if (themeId !== theme.get("id") && theme.get("component")) {
|
||||
themes.push(theme);
|
||||
}
|
||||
});
|
||||
|
||||
return themes;
|
||||
},
|
||||
|
||||
@computed("model.component")
|
||||
switchKey(component) {
|
||||
convertKey(component) {
|
||||
const type = component ? "component" : "theme";
|
||||
return `admin.customize.theme.switch_${type}`;
|
||||
return `admin.customize.theme.convert_${type}`;
|
||||
},
|
||||
|
||||
@computed("model.component")
|
||||
convertIcon(component) {
|
||||
return component ? "cube" : "";
|
||||
},
|
||||
|
||||
@computed("model.component")
|
||||
convertTooltip(component) {
|
||||
const type = component ? "component" : "theme";
|
||||
return `admin.customize.theme.convert_${type}_tooltip`;
|
||||
},
|
||||
|
||||
@computed("model.settings")
|
||||
@ -126,8 +95,65 @@ export default Ember.Controller.extend({
|
||||
return settings.length > 0;
|
||||
},
|
||||
|
||||
downloadUrl: url("model.id", "/admin/themes/%@"),
|
||||
@computed("model.remoteError", "updatingRemote")
|
||||
showRemoteError(errorMessage, updating) {
|
||||
return errorMessage && !updating;
|
||||
},
|
||||
|
||||
editedFieldsForTarget(target) {
|
||||
return this.get("model.editedFields").filter(
|
||||
field => field.target === target
|
||||
);
|
||||
},
|
||||
|
||||
commitSwitchType() {
|
||||
const model = this.get("model");
|
||||
const newValue = !model.get("component");
|
||||
model.set("component", newValue);
|
||||
|
||||
if (newValue) {
|
||||
this.set("parentController.currentTab", COMPONENTS);
|
||||
} else {
|
||||
this.set("parentController.currentTab", THEMES);
|
||||
}
|
||||
|
||||
model
|
||||
.saveChanges("component")
|
||||
.then(() => {
|
||||
this.set("colorSchemeId", null);
|
||||
|
||||
model.setProperties({
|
||||
default: false,
|
||||
color_scheme_id: null,
|
||||
user_selectable: false,
|
||||
child_themes: [],
|
||||
childThemes: []
|
||||
});
|
||||
|
||||
this.get("parentController.model.content").forEach(theme => {
|
||||
const children = _.toArray(theme.get("childThemes"));
|
||||
const rawChildren = _.toArray(theme.get("child_themes") || []);
|
||||
const index = children ? children.indexOf(model) : -1;
|
||||
if (index > -1) {
|
||||
children.splice(index, 1);
|
||||
rawChildren.splice(index, 1);
|
||||
theme.setProperties({
|
||||
childThemes: children,
|
||||
child_themes: rawChildren
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
transitionToEditRoute() {
|
||||
this.transitionToRoute(
|
||||
this.get("editRouteName"),
|
||||
this.get("model.id"),
|
||||
"common",
|
||||
"scss"
|
||||
);
|
||||
},
|
||||
actions: {
|
||||
updateToLatest() {
|
||||
this.set("updatingRemote", true);
|
||||
@ -184,25 +210,17 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
editTheme() {
|
||||
let edit = () =>
|
||||
this.transitionToRoute(
|
||||
this.get("editRouteName"),
|
||||
this.get("model.id"),
|
||||
"common",
|
||||
"scss"
|
||||
);
|
||||
|
||||
if (this.get("model.remote_theme")) {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.theme.edit_confirm"),
|
||||
result => {
|
||||
if (result) {
|
||||
edit();
|
||||
this.transitionToEditRoute();
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
edit();
|
||||
this.transitionToEditRoute();
|
||||
}
|
||||
},
|
||||
|
||||
@ -264,30 +282,26 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
switchType() {
|
||||
return bootbox.confirm(
|
||||
I18n.t(`${this.get("switchKey")}_alert`),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
result => {
|
||||
if (result) {
|
||||
const model = this.get("model");
|
||||
model.set("component", !model.get("component"));
|
||||
model
|
||||
.saveChanges("component")
|
||||
.then(() => {
|
||||
this.set("colorSchemeId", null);
|
||||
model.setProperties({
|
||||
default: false,
|
||||
color_scheme_id: null,
|
||||
user_selectable: false,
|
||||
child_themes: [],
|
||||
childThemes: []
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
const relatives = this.get("model.component")
|
||||
? this.get("parentThemes")
|
||||
: this.get("model.childThemes");
|
||||
if (relatives && relatives.length > 0) {
|
||||
const names = relatives.map(relative => relative.get("name"));
|
||||
bootbox.confirm(
|
||||
I18n.t(`${this.get("convertKey")}_alert`, {
|
||||
relatives: names.join(", ")
|
||||
}),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
result => {
|
||||
if (result) {
|
||||
this.commitSwitchType();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
} else {
|
||||
this.commitSwitchType();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import { THEMES } from "admin/models/theme";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed("model", "model.@each", "model.@each.component")
|
||||
currentTab: THEMES,
|
||||
|
||||
@computed("model", "model.@each.component")
|
||||
fullThemes(themes) {
|
||||
return _.sortBy(themes.filter(t => !t.get("component")), t => {
|
||||
return [
|
||||
!t.get("default"),
|
||||
!t.get("user_selectable"),
|
||||
t.get("name").toLowerCase()
|
||||
];
|
||||
});
|
||||
return themes.filter(t => !t.get("component"));
|
||||
},
|
||||
|
||||
@computed("model", "model.@each", "model.@each.component")
|
||||
@computed("model", "model.@each.component")
|
||||
childThemes(themes) {
|
||||
return themes.filter(t => t.get("component"));
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
editingName: false,
|
||||
editingTitle: false,
|
||||
originalPrimaryGroupId: null,
|
||||
customGroupIdsBuffer: null,
|
||||
availableGroups: null,
|
||||
userTitleValue: null,
|
||||
|
||||
@ -30,6 +31,20 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
"model.can_disable_second_factor"
|
||||
),
|
||||
|
||||
@computed("model.customGroups")
|
||||
customGroupIds(customGroups) {
|
||||
return customGroups.mapBy("id");
|
||||
},
|
||||
|
||||
@computed("customGroupIdsBuffer", "customGroupIds")
|
||||
customGroupsDirty(buffer, original) {
|
||||
if (buffer === null) return false;
|
||||
|
||||
return buffer.length === original.length
|
||||
? buffer.any(id => !original.includes(id))
|
||||
: true;
|
||||
},
|
||||
|
||||
@computed("model.automaticGroups")
|
||||
automaticGroups(automaticGroups) {
|
||||
return automaticGroups
|
||||
@ -105,6 +120,27 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
}
|
||||
},
|
||||
|
||||
groupAdded(added) {
|
||||
this.get("model")
|
||||
.groupAdded(added)
|
||||
.catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
},
|
||||
|
||||
groupRemoved(groupId) {
|
||||
this.get("model")
|
||||
.groupRemoved(groupId)
|
||||
.then(() => {
|
||||
if (groupId === this.get("originalPrimaryGroupId")) {
|
||||
this.set("originalPrimaryGroupId", null);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
impersonate() {
|
||||
return this.get("model").impersonate();
|
||||
@ -278,20 +314,22 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
this.get("model").generateApiKey();
|
||||
},
|
||||
|
||||
groupAdded(added) {
|
||||
this.get("model")
|
||||
.groupAdded(added)
|
||||
.catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
saveCustomGroups() {
|
||||
const currentIds = this.get("customGroupIds");
|
||||
const bufferedIds = this.get("customGroupIdsBuffer");
|
||||
const availableGroups = this.get("availableGroups");
|
||||
|
||||
bufferedIds.filter(id => !currentIds.includes(id)).forEach(id => {
|
||||
this.groupAdded(availableGroups.findBy("id", id));
|
||||
});
|
||||
|
||||
currentIds
|
||||
.filter(id => !bufferedIds.includes(id))
|
||||
.forEach(id => this.groupRemoved(id));
|
||||
},
|
||||
|
||||
groupRemoved(groupId) {
|
||||
this.get("model")
|
||||
.groupRemoved(groupId)
|
||||
.catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
resetCustomGroups() {
|
||||
this.set("customGroupIdsBuffer", null);
|
||||
},
|
||||
|
||||
savePrimaryGroup() {
|
||||
|
||||
@ -3,9 +3,14 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@computed
|
||||
showBadges() {
|
||||
return this.currentUser.get("admin") && this.siteSettings.enable_badges;
|
||||
@computed("siteSettings.enable_group_directory")
|
||||
showGroups(enableGroupDirectory) {
|
||||
return !enableGroupDirectory;
|
||||
},
|
||||
|
||||
@computed("siteSettings.enable_badges")
|
||||
showBadges(enableBadges) {
|
||||
return this.currentUser.get("admin") && enableBadges;
|
||||
},
|
||||
|
||||
@computed("application.currentPath")
|
||||
|
||||
@ -1,27 +1,56 @@
|
||||
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 COMPONENT = "component";
|
||||
const MIN_NAME_LENGTH = 4;
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
types: [
|
||||
{ name: I18n.t("admin.customize.theme.theme"), value: "theme" },
|
||||
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENT }
|
||||
],
|
||||
selectedType: "theme",
|
||||
name: I18n.t("admin.customize.new_style"),
|
||||
saving: false,
|
||||
triggerError: false,
|
||||
themesController: Ember.inject.controller("adminCustomizeThemes"),
|
||||
loading: false,
|
||||
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 === COMPONENT;
|
||||
return type === COMPONENTS;
|
||||
},
|
||||
|
||||
actions: {
|
||||
createTheme() {
|
||||
this.set("loading", true);
|
||||
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") })
|
||||
@ -30,7 +59,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("loading", false));
|
||||
.finally(() => this.set("saving", false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -44,7 +44,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
options.data.append("theme", $("#file-input")[0].files[0]);
|
||||
} else {
|
||||
options.data = {
|
||||
remote: this.get("uploadUrl")
|
||||
remote: this.get("uploadUrl"),
|
||||
branch: this.get("branch")
|
||||
};
|
||||
|
||||
if (this.get("privateChecked")) {
|
||||
|
||||
@ -1,24 +1,17 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import Backup from "admin/models/backup";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
adminBackupsLogs: Ember.inject.controller(),
|
||||
|
||||
_startBackup(withUploads) {
|
||||
this.currentUser.set("hideReadOnlyAlert", true);
|
||||
Backup.start(withUploads).then(() => {
|
||||
this.get("adminBackupsLogs.logs").clear();
|
||||
this.send("backupStarted");
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
startBackup() {
|
||||
this._startBackup();
|
||||
startBackupWithUploads() {
|
||||
this.send("closeModal");
|
||||
this.send("startBackup", true);
|
||||
},
|
||||
|
||||
startBackupWithoutUpload() {
|
||||
this._startBackup(false);
|
||||
startBackupWithoutUploads() {
|
||||
this.send("closeModal");
|
||||
this.send("startBackup", false);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
|
||||
@ -4,8 +4,15 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
export const THEMES = "themes";
|
||||
export const COMPONENTS = "components";
|
||||
const SETTINGS_TYPE_ID = 5;
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
FIELDS_IDS: [0, 1],
|
||||
isActive: Em.computed.or("default", "user_selectable"),
|
||||
isPendingUpdates: Em.computed.gt("remote_theme.commits_behind", 0),
|
||||
hasEditedFields: Em.computed.gt("editedFields.length", 0),
|
||||
|
||||
@computed("theme_fields")
|
||||
themeFields(fields) {
|
||||
@ -33,6 +40,27 @@ const Theme = RestModel.extend({
|
||||
);
|
||||
},
|
||||
|
||||
@computed("theme_fields", "theme_fields.@each.error")
|
||||
isBroken(fields) {
|
||||
return (
|
||||
fields && fields.some(field => field.error && field.error.length > 0)
|
||||
);
|
||||
},
|
||||
|
||||
@computed("theme_fields.@each")
|
||||
editedFields(fields) {
|
||||
return fields.filter(
|
||||
field => !Em.isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID
|
||||
);
|
||||
},
|
||||
|
||||
@computed("remote_theme.last_error_text")
|
||||
remoteError(errorText) {
|
||||
if (errorText && errorText.length > 0) {
|
||||
return errorText;
|
||||
}
|
||||
},
|
||||
|
||||
getKey(field) {
|
||||
return `${field.target} ${field.name}`;
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.messageBus.subscribe(LOG_CHANNEL, log => {
|
||||
if (log.message === "[STARTED]") {
|
||||
Discourse.User.currentProp("hideReadOnlyAlert", true);
|
||||
this.controllerFor("adminBackups").set(
|
||||
"model.isOperationRunning",
|
||||
true
|
||||
@ -62,15 +63,14 @@ export default Discourse.Route.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
startBackup() {
|
||||
showStartBackupModal() {
|
||||
showModal("admin-start-backup", { admin: true });
|
||||
this.controllerFor("modal").set("modalClass", "start-backup-modal");
|
||||
},
|
||||
|
||||
backupStarted() {
|
||||
this.controllerFor("adminBackups").set("isOperationRunning", true);
|
||||
startBackup(withUploads) {
|
||||
this.transitionTo("admin.backups.logs");
|
||||
this.send("closeModal");
|
||||
Backup.start(withUploads);
|
||||
},
|
||||
|
||||
destroyBackup(backup) {
|
||||
@ -100,17 +100,8 @@ export default Discourse.Route.extend({
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
Discourse.User.currentProp("hideReadOnlyAlert", true);
|
||||
backup.restore().then(function() {
|
||||
self
|
||||
.controllerFor("adminBackupsLogs")
|
||||
.get("logs")
|
||||
.clear();
|
||||
self
|
||||
.controllerFor("adminBackups")
|
||||
.set("model.isOperationRunning", true);
|
||||
self.transitionTo("admin.backups.logs");
|
||||
});
|
||||
self.transitionTo("admin.backups.logs");
|
||||
backup.restore();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -35,5 +35,29 @@ export default Ember.Route.extend({
|
||||
controller.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
this.controllerFor("adminCustomizeThemes").set("editingTheme", true);
|
||||
this.set("shouldAlertUnsavedChanges", true);
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
if (
|
||||
this.get("controller.model.changed") &&
|
||||
this.get("shouldAlertUnsavedChanges") &&
|
||||
transition.intent.name !== this.routeName
|
||||
) {
|
||||
transition.abort();
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.customize.theme.unsaved_changes_alert"),
|
||||
I18n.t("admin.customize.theme.discard"),
|
||||
I18n.t("admin.customize.theme.stay"),
|
||||
result => {
|
||||
if (!result) {
|
||||
this.set("shouldAlertUnsavedChanges", false);
|
||||
transition.retry();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,30 @@
|
||||
import { emojiUrlFor } from "discourse/lib/text";
|
||||
|
||||
const externalResources = [
|
||||
{
|
||||
key: "admin.customize.theme.beginners_guide_title",
|
||||
link: "https://meta.discourse.org/t/91966",
|
||||
icon: "book"
|
||||
},
|
||||
{
|
||||
key: "admin.customize.theme.developers_guide_title",
|
||||
link: "https://meta.discourse.org/t/93648",
|
||||
icon: "book"
|
||||
},
|
||||
{
|
||||
key: "admin.customize.theme.browse_themes",
|
||||
link: "https://meta.discourse.org/c/theme",
|
||||
icon: "paint-brush"
|
||||
}
|
||||
];
|
||||
|
||||
export default Ember.Route.extend({
|
||||
setupController() {
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
this.controllerFor("adminCustomizeThemes").set("editingTheme", false);
|
||||
controller.setProperties({
|
||||
externalResources,
|
||||
womanArtistEmojiURL: emojiUrlFor("woman_artist:t5")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
||||
import { THEMES, COMPONENTS } from "admin/models/theme";
|
||||
|
||||
export default Ember.Route.extend({
|
||||
serialize(model) {
|
||||
@ -14,19 +15,21 @@ export default Ember.Route.extend({
|
||||
setupController(controller, model) {
|
||||
this._super(...arguments);
|
||||
|
||||
controller.set("model", model);
|
||||
|
||||
const parentController = this.controllerFor("adminCustomizeThemes");
|
||||
parentController.set("editingTheme", false);
|
||||
controller.set("allThemes", parentController.get("model"));
|
||||
parentController.setProperties({
|
||||
editingTheme: false,
|
||||
currentTab: model.get("component") ? COMPONENTS : THEMES
|
||||
});
|
||||
|
||||
controller.setProperties({
|
||||
model: model,
|
||||
parentController: parentController,
|
||||
allThemes: parentController.get("model"),
|
||||
colorSchemeId: model.get("color_scheme_id"),
|
||||
colorSchemes: parentController.get("model.extras.color_schemes")
|
||||
});
|
||||
|
||||
this.handleHighlight(model);
|
||||
|
||||
controller.set(
|
||||
"colorSchemes",
|
||||
parentController.get("model.extras.color_schemes")
|
||||
);
|
||||
controller.set("colorSchemeId", model.get("color_scheme_id"));
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
@ -34,9 +37,11 @@ export default Ember.Route.extend({
|
||||
},
|
||||
|
||||
handleHighlight(theme) {
|
||||
this.get("controller.allThemes").forEach(t => t.set("active", false));
|
||||
this.get("controller.allThemes")
|
||||
.filter(t => t.get("selected"))
|
||||
.forEach(t => t.set("selected", false));
|
||||
if (theme) {
|
||||
theme.set("active", true);
|
||||
theme.set("selected", true);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
{{nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminUsersList' label='admin.users.title'}}
|
||||
{{#if showGroups}}
|
||||
{{nav-item route='groups' label='admin.groups.title'}}
|
||||
{{/if}}
|
||||
{{#if showBadges}}
|
||||
{{nav-item route='adminBadges' label='admin.badges.title'}}
|
||||
{{/if}}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
label="admin.backups.operations.cancel.label"
|
||||
icon="times"}}
|
||||
{{else}}
|
||||
{{d-button action="startBackup"
|
||||
{{d-button action="showStartBackupModal"
|
||||
class="btn-primary"
|
||||
title="admin.backups.operations.backup.title"
|
||||
label="admin.backups.operations.backup.label"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<label>
|
||||
{{input type="checkbox" checked=enabled}}
|
||||
{{{unbound setting.description}}}
|
||||
<span>{{{unbound setting.description}}}</span>
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
</label>
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<div class="inner-wrapper">
|
||||
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
|
||||
|
||||
<div class="info">
|
||||
<span class="name">
|
||||
{{theme.name}}
|
||||
</span>
|
||||
|
||||
<span class="icons">
|
||||
{{#unless theme.selected}}
|
||||
{{#if theme.default}}
|
||||
{{d-icon "check" class="default-indicator" title="admin.customize.theme.default_theme_tooltip"}}
|
||||
{{/if}}
|
||||
{{#if theme.isPendingUpdates}}
|
||||
{{d-icon "refresh" title="admin.customize.theme.updates_available_tooltip" class="light-grey-icon"}}
|
||||
{{/if}}
|
||||
{{#if theme.isBroken}}
|
||||
{{d-icon "exclamation-circle" class="broken-indicator" title="admin.customize.theme.broken_theme_tooltip"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{d-icon "caret-right"}}
|
||||
{{/unless}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{#if displayComponents}}
|
||||
<div class="components-list">
|
||||
{{#each children as |child|}}
|
||||
<span class="component">
|
||||
{{child}}
|
||||
</span>
|
||||
{{/each}}
|
||||
{{#if displayHasMore}}
|
||||
<span {{action "toggleChildrenExpanded"}} class="others-count">
|
||||
{{#if childrenExpanded}}
|
||||
{{I18n "admin.customize.theme.collapse"}}
|
||||
{{else}}
|
||||
{{I18n "admin.customize.theme.and_x_more" count=moreCount}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -1,23 +1,37 @@
|
||||
<div class="themes-list-header">
|
||||
<b>{{I18n title}}</b>
|
||||
<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' ''}}">
|
||||
{{I18n "admin.customize.theme.components"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="themes-list-container">
|
||||
{{#if hasThemes}}
|
||||
{{#each themes as |theme|}}
|
||||
<div class="themes-list-item {{if theme.active 'active' ''}}">
|
||||
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
|
||||
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
|
||||
{{theme.name}}
|
||||
{{#if theme.user_selectable}}
|
||||
{{d-icon "user"}}
|
||||
{{/if}}
|
||||
{{#if theme.default}}
|
||||
{{d-icon "asterisk"}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#if componentsTabActive}}
|
||||
{{#each themesList as |theme|}}
|
||||
{{themes-list-item theme=theme navigateToTheme=(action "navigateToTheme" theme)}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#if hasUserThemes}}
|
||||
{{#each userThemes as |theme|}}
|
||||
{{themes-list-item theme=theme navigateToTheme=(action "navigateToTheme" theme)}}
|
||||
{{/each}}
|
||||
|
||||
{{#if hasInactiveThemes}}
|
||||
<div class="themes-list-item inactive-indicator">
|
||||
<span class="empty">{{I18n "admin.customize.theme.inactive_themes"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if hasInactiveThemes}}
|
||||
{{#each inactiveThemes as |theme|}}
|
||||
{{themes-list-item theme=theme navigateToTheme=(action "navigateToTheme" theme)}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="themes-list-item">
|
||||
<span class="empty">{{I18n "admin.customize.theme.empty"}}</span>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
icon="times"
|
||||
class="remove-value-btn btn-small"}}
|
||||
|
||||
{{input value=value class="value-input" focus-out=(action "changeValue" index)}}
|
||||
{{input title=value value=value class="value-input" focus-out=(action "changeValue" index)}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -1 +1,14 @@
|
||||
<p class="about">{{i18n 'admin.customize.about'}}</p>
|
||||
<div class="themes-intro">
|
||||
<img src={{womanArtistEmojiURL}}>
|
||||
<div class="content-wrapper">
|
||||
<h1>{{I18n "admin.customize.theme.themes_intro"}}</h1>
|
||||
<div class="external-resources">
|
||||
{{#each externalResources as |resource|}}
|
||||
<a href={{resource.link}} class="external-link" target="_blank">
|
||||
{{d-icon resource.icon}}
|
||||
{{I18n resource.key}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div class="show-current-style">
|
||||
<h1>
|
||||
<div class="title">
|
||||
{{#if editingName}}
|
||||
{{text-field value=model.name autofocus="true"}}
|
||||
{{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
@ -7,65 +7,71 @@
|
||||
{{else}}
|
||||
{{model.name}} <a {{action "startEditingName"}}>{{d-icon "pencil"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{{#each model.errors as |error|}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
{{error}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if model.remote_theme}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
|
||||
</p>
|
||||
{{#if model.remote_theme.license_url}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}}</a>
|
||||
</p>
|
||||
<a class="url about-url" href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
|
||||
{{#if model.remote_theme.license_url}}
|
||||
<a class="url license-url" href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}}</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if parentThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
|
||||
<ul>
|
||||
{{#each parentThemes as |theme|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.component_of"}}</div>
|
||||
<ul>
|
||||
{{#each parentThemes as |theme|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless model.component}}
|
||||
<p>
|
||||
<div class="control-unit">
|
||||
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
|
||||
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
|
||||
<p>{{combo-box content=colorSchemes
|
||||
filterable=true
|
||||
forceEscape=true
|
||||
value=colorSchemeId
|
||||
icon="paint-brush"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
<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"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.css_html"}}</div>
|
||||
{{#if model.hasEditedFields}}
|
||||
<div class="description">{{i18n "admin.customize.theme.custom_sections"}}</div>
|
||||
<ul>
|
||||
{{#each editedFieldsFormatted as |field|}}
|
||||
<li>{{field}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
|
||||
@ -75,7 +81,6 @@
|
||||
{{/if}}
|
||||
|
||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||
|
||||
{{#if model.remote_theme}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
@ -89,69 +94,75 @@
|
||||
</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{#unless showRemoteError}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{#if showRemoteError}}
|
||||
<div class="error-message">
|
||||
{{d-icon "exclamation-triangle"}} {{I18n "admin.customize.theme.repo_unreachable"}}
|
||||
</div>
|
||||
<div class="raw-error">
|
||||
<code>{{model.remoteError}}</code>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.uploads"}}</h3>
|
||||
{{#if model.uploads}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.uploads as |upload|}}
|
||||
<li>
|
||||
<span class='col'>${{upload.name}}: <a href={{upload.url}} target='_blank'>{{upload.filename}}</a></span>
|
||||
<span class='col'>
|
||||
{{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.customize.theme.no_uploads"}}</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.uploads"}}</div>
|
||||
{{#if model.uploads}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.uploads as |upload|}}
|
||||
<li>
|
||||
<span class='col'>${{upload.name}}: <a href={{upload.url}} target='_blank'>{{upload.filename}}</a></span>
|
||||
<span class='col'>
|
||||
{{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}}
|
||||
</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="description">{{i18n "admin.customize.theme.no_uploads"}}</div>
|
||||
{{/if}}
|
||||
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{#if hasSettings}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_settings"}}</h3>
|
||||
{{#d-section class="form-horizontal theme settings"}}
|
||||
{{#each settings as |setting|}}
|
||||
{{theme-setting setting=setting model=model class="theme-setting"}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.theme_settings"}}</div>
|
||||
{{#d-section class="form-horizontal theme settings"}}
|
||||
{{#each settings as |setting|}}
|
||||
{{theme-setting setting=setting model=model class="theme-setting"}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if availableChildThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
<p>
|
||||
<label class='checkbox-label'>
|
||||
{{input type="checkbox" checked=allowChildThemes}}
|
||||
{{i18n "admin.customize.theme.child_themes_check"}}
|
||||
</label>
|
||||
</p>
|
||||
{{else}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/unless}}
|
||||
{{#if selectableChildThemes}}
|
||||
<p>
|
||||
{{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId}}
|
||||
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<div class="control-unit">
|
||||
<div class="mini-title">{{i18n "admin.customize.theme.theme_components"}}</div>
|
||||
{{#if model.childThemes.length}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{#if selectableChildThemes}}
|
||||
<div class="description">
|
||||
{{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId none="admin.customize.theme.select_component"}}
|
||||
{{#d-button action="addChildTheme" icon="plus" disabled=addButtonDisabled class="add-component-button"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<a href='{{previewUrl}}' title="{{i18n 'admin.customize.explain_preview'}}" target='_blank' class='btn'>{{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}}</a>
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
{{d-button action="switchType" label=switchKey icon="arrows-h" class="btn-danger"}}
|
||||
{{d-button action="switchType" label="admin.customize.theme.convert" icon=convertIcon class="btn-normal" title=convertTooltip}}
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
{{#unless editingTheme}}
|
||||
<div class='content-list'>
|
||||
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
|
||||
<div class='customize-themes-header'>
|
||||
|
||||
<div class="create-actions">
|
||||
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
|
||||
<div class="create-actions">
|
||||
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{themes-list themes=fullThemes title="admin.customize.theme.title"}}
|
||||
{{themes-list themes=childThemes title="admin.customize.theme.components"}}
|
||||
|
||||
</div>
|
||||
{{themes-list themes=fullThemes components=childThemes currentTab=currentTab}}
|
||||
{{/unless}}
|
||||
{{outlet}}
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
{{else}}
|
||||
{{i18n "admin.logs.staff_actions.filter"}} {{combo-box content=userHistoryActions value=filterActionId none="admin.logs.staff_actions.all"}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@ -57,8 +57,14 @@
|
||||
<tr class='admin-list-item'>
|
||||
<td class="staff-users">
|
||||
<div class="staff-user">
|
||||
{{#link-to 'adminUser' item.acting_user}}{{avatar item.acting_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByStaffUser" item.acting_user}}>{{item.acting_user.username}}</a>
|
||||
{{#if item.acting_user}}
|
||||
{{#link-to 'adminUser' item.acting_user}}{{avatar item.acting_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByStaffUser" item.acting_user}}>{{item.acting_user.username}}</a>
|
||||
{{else}}
|
||||
<span class="deleted-user" title="{{i18n 'admin.user.deleted'}}">
|
||||
{{d-icon "trash-o"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col value action">
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}}
|
||||
{{#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}}
|
||||
{{input value=name placeholder=placeholder}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -16,9 +16,15 @@
|
||||
{{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="createTheme" disabled=loading}}
|
||||
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action="createTheme" disabled=saving}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
</div>
|
||||
|
||||
@ -14,26 +14,31 @@
|
||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||
{{#if remote}}
|
||||
<div class="inputs">
|
||||
{{input value=uploadUrl placeholder="https://github.com/discourse/discourse/sample_theme"}}
|
||||
<div class='repo'>
|
||||
{{input value=uploadUrl placeholder="https://github.com/discourse/sample_theme"}}
|
||||
<span class="description">{{i18n 'admin.customize.theme.import_web_tip'}}</span>
|
||||
</div>
|
||||
<div class='branch'>
|
||||
{{input value=branch placeholder="beta"}}
|
||||
<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}}
|
||||
<div class='check-private'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=privateChecked}}
|
||||
{{i18n 'admin.customize.theme.is_private'}}
|
||||
</label>
|
||||
{{#if privateChecked}}
|
||||
{{#if publicKey}}
|
||||
<div class='public-key'>
|
||||
{{i18n 'admin.customize.theme.public_key'}}
|
||||
{{textarea disabled=true value=publicKey}}
|
||||
{{textarea readonly=true value=publicKey}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#d-modal-body title="admin.backups.operations.backup.confirm"}}
|
||||
<button {{action "startBackup"}} class="btn btn-primary">{{i18n 'yes_value'}}</button>
|
||||
<button {{action "startBackupWithoutUpload"}} class="btn">{{i18n 'admin.backups.operations.backup.without_uploads'}}</button>
|
||||
<button {{action "startBackupWithUploads"}} class="btn btn-primary">{{i18n 'yes_value'}}</button>
|
||||
<button {{action "startBackupWithoutUploads"}} class="btn">{{i18n 'admin.backups.operations.backup.without_uploads'}}</button>
|
||||
<button {{action "cancel"}} class="btn">{{i18n 'no_value'}}</button>
|
||||
{{/d-modal-body}}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</td>
|
||||
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.searches'}}</div>{{item.searches}}</td>
|
||||
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.click_through'}}</div>{{item.click_through}}</td>
|
||||
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.unique'}}</div>{{item.unique}}</td>
|
||||
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.unique_searches'}}</div>{{item.unique_searches}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
|
||||
@ -459,19 +459,29 @@
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
|
||||
<div class='value'>
|
||||
{{admin-group-selector selected=model.customGroups available=availableGroups}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#if model.customGroups}}
|
||||
{{i18n 'admin.groups.primary'}}
|
||||
{{combo-box content=model.customGroups value=model.primary_group_id none="admin.groups.no_primary"}}
|
||||
{{/if}}
|
||||
{{#if primaryGroupDirty}}
|
||||
{{d-button icon="check" class="ok" action="savePrimaryGroup"}}
|
||||
{{d-button icon="times" class="cancel" action="resetPrimaryGroup"}}
|
||||
{{/if}}
|
||||
{{admin-group-selector selected=model.customGroups available=availableGroups buffer=customGroupIdsBuffer}}
|
||||
</div>
|
||||
{{#if customGroupsDirty}}
|
||||
<div class='controls'>
|
||||
{{d-button icon="check" class="ok" action="saveCustomGroups"}}
|
||||
{{d-button icon="times" class="cancel" action="resetCustomGroups"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if model.customGroups}}
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.primary'}}</div>
|
||||
<div class='value'>
|
||||
{{combo-box content=model.customGroups value=model.primary_group_id none="admin.groups.no_primary"}}
|
||||
</div>
|
||||
{{#if primaryGroupDirty}}
|
||||
<div class='controls'>
|
||||
{{d-button icon="check" class="ok" action="savePrimaryGroup"}}
|
||||
{{d-button icon="times" class="cancel" action="resetPrimaryGroup"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -16,7 +16,9 @@
|
||||
{{#unless siteSettings.enable_sso}}
|
||||
{{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
|
||||
{{/unless}}
|
||||
{{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
|
||||
{{#if currentUser.admin}}
|
||||
{{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@ -73,7 +73,6 @@
|
||||
//= require ./discourse/controllers/discovery-sortable
|
||||
//= require ./discourse/controllers/navigation/default
|
||||
//= require ./discourse/components/edit-category-panel
|
||||
//= require ./discourse/components/dropdown-button
|
||||
//= require ./discourse/lib/link-mentions
|
||||
//= require ./discourse/components/site-header
|
||||
//= require ./discourse/components/d-editor
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
(function() {
|
||||
const authenticationData = JSON.parse(
|
||||
document.getElementById("data-authentication").dataset.authenticationData
|
||||
);
|
||||
|
||||
Discourse.showingSignup = true;
|
||||
require("discourse/routes/application").default.reopen({
|
||||
actions: {
|
||||
didTransition: function() {
|
||||
Em.run.next(function() {
|
||||
Discourse.authenticationComplete(authenticationData);
|
||||
});
|
||||
return this._super();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@ -93,7 +93,7 @@ registerIconRenderer({
|
||||
let tagName = params.tagName || "i";
|
||||
let html = `<${tagName} class='${faClasses(icon, params)}'`;
|
||||
if (params.title) {
|
||||
html += ` title='${I18n.t(params.title)}'`;
|
||||
html += ` title='${I18n.t(params.title).replace(/'/g, "'")}'`;
|
||||
}
|
||||
if (params.label) {
|
||||
html += " aria-hidden='true'";
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
|
||||
|
||||
export default DropdownSelectBoxComponent.extend({
|
||||
classNames: ["auth-token-dropdown"],
|
||||
headerIcon: "wrench",
|
||||
allowInitialValueMutation: false,
|
||||
showFullTitle: false,
|
||||
|
||||
computeContent() {
|
||||
const content = [
|
||||
{
|
||||
id: "notYou",
|
||||
icon: "user-times",
|
||||
name: I18n.t("user.auth_tokens.not_you"),
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
id: "logOut",
|
||||
icon: "sign-out",
|
||||
name: I18n.t("user.log_out"),
|
||||
description: ""
|
||||
}
|
||||
];
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
actions: {
|
||||
onSelect(id) {
|
||||
switch (id) {
|
||||
case "notYou":
|
||||
this.sendAction("showToken", this.get("token"));
|
||||
break;
|
||||
case "logOut":
|
||||
this.sendAction("revokeAuthToken", this.get("token"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -3,18 +3,26 @@ import DiscourseURL from "discourse/lib/url";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: "section",
|
||||
classNameBindings: [":category-boxes", "anyLogos:with-logos:no-logos"],
|
||||
classNameBindings: [
|
||||
":category-boxes",
|
||||
"anyLogos:with-logos:no-logos",
|
||||
"hasSubcategories:with-subcategories"
|
||||
],
|
||||
|
||||
@computed("categories.[].uploaded_logo.url")
|
||||
anyLogos() {
|
||||
return this.get("categories").any(c => {
|
||||
return !Ember.isEmpty(c.get("uploaded_logo.url"));
|
||||
});
|
||||
return this.get("categories").any(
|
||||
c => !Ember.isEmpty(c.get("uploaded_logo.url"))
|
||||
);
|
||||
},
|
||||
|
||||
@computed("categories.[].subcategories")
|
||||
hasSubcategories() {
|
||||
return this.get("categories").any(
|
||||
c => !Ember.isEmpty(c.get("subcategories"))
|
||||
);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
if (!$(e.target).is("a")) {
|
||||
const url = $(e.target)
|
||||
|
||||
@ -53,10 +53,15 @@ export default Ember.Component.extend({
|
||||
_xhr: null,
|
||||
shouldBuildScrollMap: true,
|
||||
scrollMap: null,
|
||||
uploadFilenamePlaceholder: null,
|
||||
|
||||
@computed
|
||||
uploadPlaceholder() {
|
||||
return `[${I18n.t("uploading")}]() `;
|
||||
@computed("uploadFilenamePlaceholder")
|
||||
uploadPlaceholder(uploadFilenamePlaceholder) {
|
||||
const clipboard = I18n.t("clipboard");
|
||||
const filename = uploadFilenamePlaceholder
|
||||
? uploadFilenamePlaceholder
|
||||
: clipboard;
|
||||
return `[${I18n.t("uploading_filename", { filename })}]() `;
|
||||
},
|
||||
|
||||
@computed("composer.requiredCategoryMissing")
|
||||
@ -218,6 +223,53 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
_setUploadPlaceholderSend(data) {
|
||||
const filename = this._filenamePlaceholder(data);
|
||||
this.set("uploadFilenamePlaceholder", filename);
|
||||
|
||||
// when adding two separate files with the same filename search for matching
|
||||
// placeholder already existing in the editor ie [Uploading: test.png...]
|
||||
// and add order nr to the next one: [Uplodading: test.png(1)...]
|
||||
const regexString = `\\[${I18n.t("uploading_filename", {
|
||||
filename: filename + "(?:\\()?([0-9])?(?:\\))?"
|
||||
})}\\]\\(\\)`;
|
||||
const globalRegex = new RegExp(regexString, "g");
|
||||
const matchingPlaceholder = this.get("composer.reply").match(globalRegex);
|
||||
if (matchingPlaceholder) {
|
||||
// get last matching placeholder and its consecutive nr in regex
|
||||
// capturing group and apply +1 to the placeholder
|
||||
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
|
||||
const regex = new RegExp(regexString);
|
||||
const orderNr = regex.exec(lastMatch)[1]
|
||||
? parseInt(regex.exec(lastMatch)[1]) + 1
|
||||
: 1;
|
||||
data.orderNr = orderNr;
|
||||
const filenameWithOrderNr = `${filename}(${orderNr})`;
|
||||
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
|
||||
}
|
||||
},
|
||||
|
||||
_setUploadPlaceholderDone(data) {
|
||||
const filename = this._filenamePlaceholder(data);
|
||||
const filenameWithSize = `${filename} (${data.total})`;
|
||||
this.set("uploadFilenamePlaceholder", filenameWithSize);
|
||||
|
||||
if (data.orderNr) {
|
||||
const filenameWithOrderNr = `${filename}(${data.orderNr})`;
|
||||
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
|
||||
} else {
|
||||
this.set("uploadFilenamePlaceholder", filename);
|
||||
}
|
||||
},
|
||||
|
||||
_filenamePlaceholder(data) {
|
||||
return data.files[0].name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||
},
|
||||
|
||||
_resetUploadFilenamePlaceholder() {
|
||||
this.set("uploadFilenamePlaceholder", null);
|
||||
},
|
||||
|
||||
_enableAdvancedEditorPreviewSync() {
|
||||
return this.siteSettings.enable_advanced_editor_preview_sync;
|
||||
},
|
||||
@ -542,23 +594,26 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_resetUpload(removePlaceholder) {
|
||||
if (this._validUploads > 0) {
|
||||
this._validUploads--;
|
||||
}
|
||||
if (this._validUploads === 0) {
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: false,
|
||||
isCancellable: false
|
||||
});
|
||||
}
|
||||
if (removePlaceholder) {
|
||||
this.appEvents.trigger(
|
||||
"composer:replace-text",
|
||||
this.get("uploadPlaceholder"),
|
||||
""
|
||||
);
|
||||
}
|
||||
Ember.run.next(() => {
|
||||
if (this._validUploads > 0) {
|
||||
this._validUploads--;
|
||||
}
|
||||
if (this._validUploads === 0) {
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: false,
|
||||
isCancellable: false
|
||||
});
|
||||
}
|
||||
if (removePlaceholder) {
|
||||
this.appEvents.trigger(
|
||||
"composer:replace-text",
|
||||
this.get("uploadPlaceholder"),
|
||||
""
|
||||
);
|
||||
}
|
||||
this._resetUploadFilenamePlaceholder();
|
||||
});
|
||||
},
|
||||
|
||||
_bindUploadTarget() {
|
||||
@ -568,7 +623,6 @@ export default Ember.Component.extend({
|
||||
|
||||
const $element = this.$();
|
||||
const csrf = this.session.get("csrfToken");
|
||||
const uploadPlaceholder = this.get("uploadPlaceholder");
|
||||
|
||||
$element.fileupload({
|
||||
url: Discourse.getURL(
|
||||
@ -637,7 +691,13 @@ export default Ember.Component.extend({
|
||||
$element.on("fileuploadsend", (e, data) => {
|
||||
this._pasted = false;
|
||||
this._validUploads++;
|
||||
this.appEvents.trigger("composer:insert-text", uploadPlaceholder);
|
||||
|
||||
this._setUploadPlaceholderSend(data);
|
||||
|
||||
this.appEvents.trigger(
|
||||
"composer:insert-text",
|
||||
this.get("uploadPlaceholder")
|
||||
);
|
||||
|
||||
if (data.xhr && data.originalFiles.length === 1) {
|
||||
this.set("isCancellable", true);
|
||||
@ -647,13 +707,13 @@ export default Ember.Component.extend({
|
||||
|
||||
$element.on("fileuploaddone", (e, data) => {
|
||||
let upload = data.result;
|
||||
|
||||
this._setUploadPlaceholderDone(data);
|
||||
if (!this._xhr || !this._xhr._userCancelled) {
|
||||
const markdown = getUploadMarkdown(upload);
|
||||
cacheShortUploadUrl(upload.short_url, upload.url);
|
||||
this.appEvents.trigger(
|
||||
"composer:replace-text",
|
||||
uploadPlaceholder.trim(),
|
||||
this.get("uploadPlaceholder").trim(),
|
||||
markdown
|
||||
);
|
||||
this._resetUpload(false);
|
||||
@ -663,6 +723,7 @@ export default Ember.Component.extend({
|
||||
});
|
||||
|
||||
$element.on("fileuploadfail", (e, data) => {
|
||||
this._setUploadPlaceholderDone(data);
|
||||
this._resetUpload(true);
|
||||
|
||||
const userCancelled = this._xhr && this._xhr._userCancelled;
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { bufferedRender } from "discourse-common/lib/buffered-render";
|
||||
|
||||
export default Ember.Component.extend(
|
||||
bufferedRender({
|
||||
classNameBindings: [":btn-group", "hidden"],
|
||||
rerenderTriggers: ["text", "longDescription"],
|
||||
|
||||
_bindClick: function() {
|
||||
// If there's a click handler, call it
|
||||
if (this.clicked) {
|
||||
const self = this;
|
||||
this.$().on("click.dropdown-button", "ul li", function(e) {
|
||||
e.preventDefault();
|
||||
if ($(e.currentTarget).data("id") !== self.get("activeItem")) {
|
||||
self.clicked($(e.currentTarget).data("id"));
|
||||
}
|
||||
self.$(".dropdown-toggle").dropdown("toggle");
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}.on("didInsertElement"),
|
||||
|
||||
_unbindClick: function() {
|
||||
this.$().off("click.dropdown-button", "ul li");
|
||||
}.on("willDestroyElement"),
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const title = this.get("title");
|
||||
if (title) {
|
||||
buffer.push("<h4 class='title'>" + title + "</h4>");
|
||||
}
|
||||
|
||||
buffer.push(
|
||||
`<button class='btn standard dropdown-toggle ${this.get(
|
||||
"buttonExtraClasses"
|
||||
) || ""}' data-toggle='dropdown'>${this.get("text")}</button>`
|
||||
);
|
||||
buffer.push("<ul class='dropdown-menu'>");
|
||||
|
||||
const contents = this.get("dropDownContent");
|
||||
if (contents) {
|
||||
const self = this;
|
||||
contents.forEach(function(row) {
|
||||
const id = row.id,
|
||||
className = self.get("activeItem") === id ? "disabled" : "";
|
||||
|
||||
buffer.push(
|
||||
'<li data-id="' + id + '" class="' + className + '"><a href>'
|
||||
);
|
||||
|
||||
if (row.icon) {
|
||||
let iconClass = "icon";
|
||||
if (row.iconClass) {
|
||||
iconClass += ` ${row.iconClass}`;
|
||||
}
|
||||
buffer.push(
|
||||
iconHTML(row.icon, { tagName: "span", class: iconClass })
|
||||
);
|
||||
}
|
||||
|
||||
buffer.push("<div><span class='title'>" + row.title + "</span>");
|
||||
buffer.push("<span>" + row.description + "</span></div>");
|
||||
buffer.push("</a></li>");
|
||||
});
|
||||
}
|
||||
|
||||
buffer.push("</ul>");
|
||||
|
||||
const desc = this.get("longDescription");
|
||||
if (desc) {
|
||||
buffer.push("<p>");
|
||||
buffer.push(desc);
|
||||
buffer.push("</p>");
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -1,19 +1,22 @@
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import DropdownButton from "discourse/components/dropdown-button";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
|
||||
|
||||
export default DropdownButton.extend({
|
||||
buttonExtraClasses: "no-text",
|
||||
title: "",
|
||||
text: iconHTML("wrench"),
|
||||
classNames: ["group-member-dropdown"],
|
||||
export default DropdownSelectBoxComponent.extend({
|
||||
pluginApiIdentifiers: ["group-member-dropdown"],
|
||||
classNames: "group-member-dropdown",
|
||||
showFullTitle: false,
|
||||
allowInitialValueMutation: false,
|
||||
allowAutoSelectFirst: false,
|
||||
headerIcon: ["wrench"],
|
||||
|
||||
autoHighlight() {},
|
||||
|
||||
@computed("member.owner")
|
||||
dropDownContent(isOwner) {
|
||||
content(isOwner) {
|
||||
const items = [
|
||||
{
|
||||
id: "removeMember",
|
||||
title: I18n.t("groups.members.remove_member"),
|
||||
name: I18n.t("groups.members.remove_member"),
|
||||
description: I18n.t("groups.members.remove_member_description", {
|
||||
username: this.get("member.username")
|
||||
}),
|
||||
@ -25,7 +28,7 @@ export default DropdownButton.extend({
|
||||
if (isOwner) {
|
||||
items.push({
|
||||
id: "removeOwner",
|
||||
title: I18n.t("groups.members.remove_owner"),
|
||||
name: I18n.t("groups.members.remove_owner"),
|
||||
description: I18n.t("groups.members.remove_owner_description", {
|
||||
username: this.get("member.username")
|
||||
}),
|
||||
@ -34,7 +37,7 @@ export default DropdownButton.extend({
|
||||
} else {
|
||||
items.push({
|
||||
id: "makeOwner",
|
||||
title: I18n.t("groups.members.make_owner"),
|
||||
name: I18n.t("groups.members.make_owner"),
|
||||
description: I18n.t("groups.members.make_owner_description", {
|
||||
username: this.get("member.username")
|
||||
}),
|
||||
@ -46,7 +49,7 @@ export default DropdownButton.extend({
|
||||
return items;
|
||||
},
|
||||
|
||||
clicked(id) {
|
||||
mutateValue(id) {
|
||||
switch (id) {
|
||||
case "removeMember":
|
||||
this.sendAction("removeMember", this.get("member"));
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import DropdownButton from "discourse/components/dropdown-button";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
|
||||
|
||||
export default DropdownButton.extend({
|
||||
buttonExtraClasses: "no-text",
|
||||
title: "",
|
||||
text: iconHTML("bars") + " " + iconHTML("caret-down"),
|
||||
classNames: ["tags-admin-menu"],
|
||||
export default DropdownSelectBoxComponent.extend({
|
||||
pluginApiIdentifiers: ["tags-admin-dropdown"],
|
||||
classNames: "tags-admin-dropdown",
|
||||
showFullTitle: false,
|
||||
allowInitialValueMutation: false,
|
||||
headerIcon: ["bars", "caret-down"],
|
||||
|
||||
@computed()
|
||||
dropDownContent() {
|
||||
autoHighlight() {},
|
||||
|
||||
computeContent() {
|
||||
const items = [
|
||||
{
|
||||
id: "manageGroups",
|
||||
title: I18n.t("tagging.manage_groups"),
|
||||
name: I18n.t("tagging.manage_groups"),
|
||||
description: I18n.t("tagging.manage_groups_description"),
|
||||
icon: "wrench"
|
||||
}
|
||||
];
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
@ -25,7 +26,7 @@ export default DropdownButton.extend({
|
||||
manageGroups: "showTagGroups"
|
||||
},
|
||||
|
||||
clicked(id) {
|
||||
this.sendAction("actionNames." + id);
|
||||
mutateValue(id) {
|
||||
this.sendAction(`actionNames.${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -162,7 +162,7 @@ export default Ember.Component.extend({
|
||||
const windowHeight = $(window).height();
|
||||
const composerHeight = $("#reply-control").height() || 0;
|
||||
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
|
||||
const bottom = $("#main").height() - maximumOffset;
|
||||
const bottom = $("body").height() - maximumOffset;
|
||||
|
||||
if (composerHeight > 0) {
|
||||
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
|
||||
@ -181,7 +181,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
click(e) {
|
||||
if ($(e.target).parents("#topic-progress").length) {
|
||||
if ($(e.target).closest("#topic-progress").length) {
|
||||
this.send("toggleExpansion");
|
||||
}
|
||||
},
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
expanded: false,
|
||||
|
||||
onShow() {
|
||||
ajax(
|
||||
userPath(`${this.get("currentUser.username_lower")}/activity.json`)
|
||||
).then(posts => {
|
||||
if (posts.length > 0) {
|
||||
this.set("latest_post", posts[0]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleExpanded() {
|
||||
this.set("expanded", !this.get("expanded"));
|
||||
},
|
||||
|
||||
highlightSecure() {
|
||||
this.send("closeModal");
|
||||
|
||||
Ember.run.next(() => {
|
||||
const $prefPasswordDiv = $(".pref-password");
|
||||
|
||||
$prefPasswordDiv.addClass("highlighted");
|
||||
$prefPasswordDiv.on("animationend", () =>
|
||||
$prefPasswordDiv.removeClass("highlighted")
|
||||
);
|
||||
|
||||
window.scrollTo({
|
||||
top: $prefPasswordDiv.offset().top,
|
||||
behavior: "smooth"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -9,6 +9,9 @@ import { findAll } from "discourse/models/login-method";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
// Number of tokens shown by default.
|
||||
const DEFAULT_AUTH_TOKENS_COUNT = 2;
|
||||
|
||||
export default Ember.Controller.extend(
|
||||
CanCheckEmails,
|
||||
PreferencesTabController,
|
||||
@ -23,6 +26,8 @@ export default Ember.Controller.extend(
|
||||
|
||||
passwordProgress: null,
|
||||
|
||||
showAllAuthTokens: false,
|
||||
|
||||
cannotDeleteAccount: Em.computed.not("currentUser.can_delete_account"),
|
||||
deleteDisabled: Em.computed.or(
|
||||
"model.isSaving",
|
||||
@ -99,6 +104,22 @@ 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 : a.seen_at < b.seen_at)
|
||||
);
|
||||
|
||||
return showAllAuthTokens
|
||||
? tokens
|
||||
: tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
|
||||
},
|
||||
|
||||
@computed("model.user_auth_tokens")
|
||||
canShowAllAuthTokens(tokens) {
|
||||
return tokens.length > DEFAULT_AUTH_TOKENS_COUNT;
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set("saved", false);
|
||||
@ -200,19 +221,26 @@ export default Ember.Controller.extend(
|
||||
});
|
||||
},
|
||||
|
||||
toggleToken(token) {
|
||||
Ember.set(token, "visible", !token.visible);
|
||||
toggleShowAllAuthTokens() {
|
||||
this.set("showAllAuthTokens", !this.get("showAllAuthTokens"));
|
||||
},
|
||||
|
||||
revokeAuthToken() {
|
||||
revokeAuthToken(token) {
|
||||
ajax(
|
||||
userPath(
|
||||
`${this.get("model.username_lower")}/preferences/revoke-auth-token`
|
||||
),
|
||||
{ type: "POST" }
|
||||
{
|
||||
type: "POST",
|
||||
data: token ? { token_id: token.id } : {}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
showToken(token) {
|
||||
showModal("auth-token", { model: token });
|
||||
},
|
||||
|
||||
connectAccount(method) {
|
||||
method.doLogin();
|
||||
}
|
||||
|
||||
@ -31,7 +31,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
"disable_jump_reply",
|
||||
"automatically_unpin_topics",
|
||||
"allow_private_messages",
|
||||
"homepage_id"
|
||||
"homepage_id",
|
||||
"hide_profile_and_presence"
|
||||
];
|
||||
|
||||
if (makeDefault) {
|
||||
|
||||
@ -16,9 +16,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
return currentUser && username === currentUser.get("username");
|
||||
},
|
||||
|
||||
@computed("viewingSelf")
|
||||
canExpandProfile(viewingSelf) {
|
||||
return viewingSelf;
|
||||
@computed("viewingSelf", "model.profile_hidden")
|
||||
canExpandProfile(viewingSelf, profileHidden) {
|
||||
return !profileHidden && viewingSelf;
|
||||
},
|
||||
|
||||
@computed("model.profileBackground")
|
||||
@ -26,8 +26,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
return !Ember.isEmpty(background.toString());
|
||||
},
|
||||
|
||||
@computed("indexStream", "viewingSelf", "forceExpand")
|
||||
collapsedInfo(indexStream, viewingSelf, forceExpand) {
|
||||
@computed("model.profile_hidden", "indexStream", "viewingSelf", "forceExpand")
|
||||
collapsedInfo(profileHidden, indexStream, viewingSelf, forceExpand) {
|
||||
if (profileHidden) {
|
||||
return true;
|
||||
}
|
||||
return (!indexStream || viewingSelf) && !forceExpand;
|
||||
},
|
||||
|
||||
|
||||
@ -127,32 +127,39 @@ export default {
|
||||
|
||||
const isInternal = DiscourseURL.isInternal(href);
|
||||
|
||||
// If we're on the same site, use the router and track via AJAX
|
||||
if (tracking && isInternal && !$link.hasClass("attachment")) {
|
||||
ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
post_id: postId,
|
||||
topic_id: topicId,
|
||||
redirect: false
|
||||
},
|
||||
dataType: "html"
|
||||
});
|
||||
DiscourseURL.routeTo(href);
|
||||
return false;
|
||||
}
|
||||
|
||||
const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1;
|
||||
const middleClicked = e.which === 2;
|
||||
const openExternalInNewTab = Discourse.User.currentProp(
|
||||
"external_links_in_new_tab"
|
||||
);
|
||||
|
||||
if (
|
||||
const openWindow =
|
||||
modifierLeftClicked ||
|
||||
middleClicked ||
|
||||
(!isInternal && openExternalInNewTab)
|
||||
) {
|
||||
(!isInternal && openExternalInNewTab);
|
||||
|
||||
// If we're on the same site, use the router and track via AJAX
|
||||
if (isInternal && !$link.hasClass("attachment")) {
|
||||
if (tracking) {
|
||||
ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
post_id: postId,
|
||||
topic_id: topicId,
|
||||
redirect: false
|
||||
},
|
||||
dataType: "html"
|
||||
});
|
||||
}
|
||||
if (openWindow) {
|
||||
window.open(destUrl, "_blank").focus();
|
||||
} else {
|
||||
DiscourseURL.routeTo(href);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (openWindow) {
|
||||
window.open(destUrl, "_blank").focus();
|
||||
} else {
|
||||
DiscourseURL.redirectTo(destUrl);
|
||||
|
||||
@ -11,7 +11,6 @@ function loadWithTag(path, cb) {
|
||||
if (Ember.Test) {
|
||||
Ember.Test.registerWaiter(() => finished);
|
||||
}
|
||||
head.appendChild(s);
|
||||
|
||||
s.onload = s.onreadystatechange = function(_, abort) {
|
||||
finished = true;
|
||||
@ -27,6 +26,8 @@ function loadWithTag(path, cb) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
head.appendChild(s);
|
||||
}
|
||||
|
||||
export function loadCSS(url) {
|
||||
@ -41,17 +42,19 @@ export default function loadScript(url, opts) {
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
// Scripts should always load from CDN
|
||||
// CSS is type text, to accept it from a CDN we would need to handle CORS
|
||||
url = opts.css ? Discourse.getURL(url) : Discourse.getURLWithCDN(url);
|
||||
|
||||
$("script").each((i, tag) => {
|
||||
const src = tag.getAttribute("src");
|
||||
|
||||
if (src && (opts.scriptTag || src !== url)) {
|
||||
_loaded[tag.getAttribute("src")] = true;
|
||||
if (src && src !== url && !_loading[src]) {
|
||||
_loaded[src] = true;
|
||||
}
|
||||
});
|
||||
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
url = Discourse.getURL(url);
|
||||
|
||||
// If we already loaded this url
|
||||
if (_loaded[url]) {
|
||||
return resolve();
|
||||
@ -78,30 +81,15 @@ export default function loadScript(url, opts) {
|
||||
_loaded[url] = true;
|
||||
};
|
||||
|
||||
let cdnUrl = url;
|
||||
|
||||
// Scripts should always load from CDN
|
||||
// CSS is type text, to accept it from a CDN we would need to handle CORS
|
||||
if (!opts.css && Discourse.CDN && url[0] === "/" && url[1] !== "/") {
|
||||
cdnUrl = Discourse.CDN.replace(/\/$/, "") + url;
|
||||
}
|
||||
|
||||
// Some javascript depends on the path of where it is loaded (ace editor)
|
||||
// to dynamically load more JS. In that case, add the `scriptTag: true`
|
||||
// option.
|
||||
if (opts.scriptTag) {
|
||||
if (Ember.testing) {
|
||||
throw new Error(
|
||||
`In test mode scripts cannot be loaded async ${cdnUrl}`
|
||||
);
|
||||
}
|
||||
loadWithTag(cdnUrl, cb);
|
||||
} else {
|
||||
if (opts.css) {
|
||||
ajax({
|
||||
url: cdnUrl,
|
||||
dataType: opts.css ? "text" : "script",
|
||||
url: url,
|
||||
dataType: "text",
|
||||
cache: true
|
||||
}).then(cb);
|
||||
} else {
|
||||
// Always load JavaScript with script tag to avoid Content Security Policy inline violations
|
||||
loadWithTag(url, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { addDecorator } from "discourse/widgets/post-cooked";
|
||||
import ComposerEditor from "discourse/components/composer-editor";
|
||||
import DiscourseBanner from "discourse/components/discourse-banner";
|
||||
import { addButton } from "discourse/widgets/post-menu";
|
||||
import { includeAttributes } from "discourse/lib/transform-post";
|
||||
import { addToolbarCallback } from "discourse/components/d-editor";
|
||||
@ -175,6 +176,7 @@ class PluginApi {
|
||||
|
||||
if (!opts.onlyStream) {
|
||||
decorate(ComposerEditor, "previewRefreshed", callback);
|
||||
decorate(DiscourseBanner, "didInsertElement", callback);
|
||||
decorate(
|
||||
this.container.factoryFor("component:user-stream").class,
|
||||
"didInsertElement",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export default function renderTag(tag, params) {
|
||||
params = params || {};
|
||||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const visibleName = Handlebars.Utils.escapeExpression(tag);
|
||||
tag = visibleName.toLowerCase();
|
||||
const classes = ["discourse-tag"];
|
||||
const tagName = params.tagName || "a";
|
||||
let path;
|
||||
@ -29,7 +30,7 @@ export default function renderTag(tag, params) {
|
||||
" class='" +
|
||||
classes.join(" ") +
|
||||
"'>" +
|
||||
tag +
|
||||
visibleName +
|
||||
"</" +
|
||||
tagName +
|
||||
">";
|
||||
|
||||
@ -187,13 +187,7 @@ const DiscourseURL = Ember.Object.extend({
|
||||
const pathname = path.replace(/(https?\:)?\/\/[^\/]+/, "");
|
||||
const baseUri = Discourse.BaseUri;
|
||||
|
||||
// If we have a baseUri and an absolute URL, make sure the baseUri
|
||||
// is the same. Otherwise we could be switching forums.
|
||||
if (
|
||||
baseUri &&
|
||||
path.indexOf("http") === 0 &&
|
||||
pathname.indexOf(baseUri) !== 0
|
||||
) {
|
||||
if (!DiscourseURL.isInternal(path)) {
|
||||
return redirectTo(path);
|
||||
}
|
||||
|
||||
@ -207,11 +201,6 @@ const DiscourseURL = Ember.Object.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
// Protocol relative URLs
|
||||
if (path.indexOf("//") === 0) {
|
||||
return redirectTo(path);
|
||||
}
|
||||
|
||||
// Scroll to the same page, different anchor
|
||||
const m = /^#(.+)$/.exec(path);
|
||||
if (m) {
|
||||
|
||||
@ -284,7 +284,8 @@ const User = RestModel.extend({
|
||||
"include_tl0_in_digests",
|
||||
"theme_ids",
|
||||
"allow_private_messages",
|
||||
"homepage_id"
|
||||
"homepage_id",
|
||||
"hide_profile_and_presence"
|
||||
];
|
||||
|
||||
if (fields) {
|
||||
|
||||
@ -102,6 +102,7 @@ export default function() {
|
||||
"user",
|
||||
{ path: "/u/:username", resetNamespace: true },
|
||||
function() {
|
||||
this.route("profile-hidden");
|
||||
this.route("summary");
|
||||
this.route(
|
||||
"userActivity",
|
||||
|
||||
@ -13,6 +13,7 @@ export default Discourse.Route.extend(OpenComposer, {
|
||||
beforeModel(transition) {
|
||||
if (
|
||||
(transition.intent.url === "/" ||
|
||||
transition.intent.url === "/latest" ||
|
||||
transition.intent.url === "/categories") &&
|
||||
transition.targetName.indexOf("discovery.top") === -1 &&
|
||||
Discourse.User.currentProp("should_be_redirected_to_top")
|
||||
|
||||
@ -11,8 +11,6 @@ export default Discourse.Route.extend({
|
||||
username: { refreshModel: true }
|
||||
},
|
||||
|
||||
refreshQueryWithoutTransition: true,
|
||||
|
||||
model(params) {
|
||||
this._params = params;
|
||||
return this.store.findAll("group", params);
|
||||
|
||||
@ -11,24 +11,17 @@ export default Discourse.Route.extend({
|
||||
category = Category.findById(category_id);
|
||||
} else if (transition.queryParams.category) {
|
||||
const splitCategory = transition.queryParams.category.split("/");
|
||||
|
||||
if (!splitCategory[1]) {
|
||||
category = this.site
|
||||
.get("categories")
|
||||
.findBy("nameLower", splitCategory[0].toLowerCase());
|
||||
} else {
|
||||
const categories = this.site.get("categories");
|
||||
const mainCategory = categories.findBy(
|
||||
"nameLower",
|
||||
splitCategory[0].toLowerCase()
|
||||
category = this._getCategory(
|
||||
splitCategory[0],
|
||||
splitCategory[1],
|
||||
"nameLower"
|
||||
);
|
||||
if (!category) {
|
||||
category = this._getCategory(
|
||||
splitCategory[0],
|
||||
splitCategory[1],
|
||||
"slug"
|
||||
);
|
||||
category = categories.find(function(item) {
|
||||
return (
|
||||
item &&
|
||||
item.get("nameLower") === splitCategory[1].toLowerCase() &&
|
||||
item.get("parent_category_id") === mainCategory.id
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (category) {
|
||||
@ -86,5 +79,27 @@ export default Discourse.Route.extend({
|
||||
self.replaceWith("login");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getCategory(mainCategory, subCategory, type) {
|
||||
let category;
|
||||
if (!subCategory) {
|
||||
category = this.site
|
||||
.get("categories")
|
||||
.findBy(type, mainCategory.toLowerCase());
|
||||
} else {
|
||||
const categories = this.site.get("categories");
|
||||
const main = categories.findBy(type, mainCategory.toLowerCase());
|
||||
if (main) {
|
||||
category = categories.find(function(item) {
|
||||
return (
|
||||
item &&
|
||||
item.get(type) === subCategory.toLowerCase() &&
|
||||
item.get("parent_category_id") === main.id
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
return category;
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,10 +49,12 @@ export default Discourse.Route.extend({
|
||||
|
||||
if (tag && tag.get("id") !== "none" && this.get("currentUser")) {
|
||||
// If logged in, we should get the tag's user settings
|
||||
return this.store.find("tagNotification", tag.get("id")).then(tn => {
|
||||
this.set("tagNotification", tn);
|
||||
return tag;
|
||||
});
|
||||
return this.store
|
||||
.find("tagNotification", tag.get("id").toLowerCase())
|
||||
.then(tn => {
|
||||
this.set("tagNotification", tn);
|
||||
return tag;
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
@ -67,7 +69,7 @@ export default Discourse.Route.extend({
|
||||
const categorySlug = this.get("categorySlug");
|
||||
const parentCategorySlug = this.get("parentCategorySlug");
|
||||
const filter = this.get("navMode");
|
||||
const tag_id = tag ? tag.id : "none";
|
||||
const tag_id = tag ? tag.id.toLowerCase() : "none";
|
||||
|
||||
if (categorySlug) {
|
||||
var category = Discourse.Category.findBySlug(
|
||||
@ -100,6 +102,9 @@ export default Discourse.Route.extend({
|
||||
params,
|
||||
{}
|
||||
).then(list => {
|
||||
if (list.topic_list.tags) {
|
||||
tag.set("id", list.topic_list.tags[0].name); // Update name of tag (case might be different)
|
||||
}
|
||||
controller.setProperties({
|
||||
list: list,
|
||||
canCreateTopic: list.get("can_create_topic"),
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.modelFor("user");
|
||||
let user = this.modelFor("user");
|
||||
if (user.get("profile_hidden")) {
|
||||
return this.replaceWith("user.profile-hidden");
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
setupController(controller, user) {
|
||||
|
||||
@ -2,7 +2,12 @@ export default Discourse.Route.extend({
|
||||
showFooter: true,
|
||||
|
||||
model() {
|
||||
return this.modelFor("user").summary();
|
||||
let user = this.modelFor("user");
|
||||
if (user.get("profile_hidden")) {
|
||||
return this.replaceWith("user.profile-hidden");
|
||||
}
|
||||
|
||||
return user.summary();
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -17,6 +17,19 @@
|
||||
<div class='description'>
|
||||
{{{text-overflow class="overflow" text=c.description_excerpt}}}
|
||||
</div>
|
||||
|
||||
{{#if c.subcategories}}
|
||||
<div class='subcategories'>
|
||||
{{#each c.subcategories as |sc|}}
|
||||
<a class="subcategory" href={{sc.url}}>
|
||||
<span class="subcategory-image-placeholder">
|
||||
{{cdn-img src=sc.uploaded_logo.url class="logo"}}
|
||||
</span>
|
||||
<span class="subcategory-link">{{sc.name}}</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
{{#each group.buttons as |b|}}
|
||||
{{#if b.popupMenu}}
|
||||
{{toolbar-popup-menu-options
|
||||
onPopupMenuAction=onPopupMenuAction
|
||||
onSelect=onPopupMenuAction
|
||||
onExpand=(action b.action b)
|
||||
title=b.title
|
||||
headerIcon=b.icon
|
||||
|
||||
@ -44,15 +44,19 @@
|
||||
|
||||
<div class='input-prepend input-append' style="margin-top: 10px;">
|
||||
<span class='color-title'>{{i18n 'category.background_color'}}:</span>
|
||||
<span class='add-on'>#</span>{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
|
||||
<div class="colorpicker-wrapper">
|
||||
<span class='add-on'>#</span>{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='input-prepend input-append'>
|
||||
<span class='color-title'>{{i18n 'category.foreground_color'}}:</span>
|
||||
<span class='add-on'>#</span>{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}}
|
||||
</div>
|
||||
<div class="colorpicker-wrapper">
|
||||
<span class='add-on'>#</span>{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/unless}}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
{{#if copied}}
|
||||
<a class="btn btn-hover pull-right">{{d-icon "copy"}} {{i18n "ip_lookup.copied"}}</a>
|
||||
{{else}}
|
||||
<a class="btn pull-right" {{action "copy"}}>{{d-icon "copy"}}</a>
|
||||
<a class="btn pull-right no-text" {{action "copy"}}>{{d-icon "copy"}}</a>
|
||||
{{/if}}
|
||||
<h4>{{i18n 'ip_lookup.title'}}</h4>
|
||||
<p class='powered-by'>{{{i18n 'ip_lookup.powered_by'}}}</p>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}}</label>
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}} {{#if field.required}}<span class='required'>*</span>{{/if}}
|
||||
</label>
|
||||
<div class='controls'>
|
||||
<label class="control-label checkbox-label">{{input id=(concat 'user-' elementId) checked=value type="checkbox"}} {{{field.description}}}</label>
|
||||
<label class="control-label checkbox-label">{{input id=(concat 'user-' elementId) checked=value type="checkbox"}} <span>{{{field.description}}}</span></label>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}}</label>
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}} {{#if field.required}}<span class='required'>*</span>{{/if}}
|
||||
</label>
|
||||
<div class='controls'>
|
||||
{{combo-box id=(concat 'user-' elementId) content=field.options value=value none=noneLabel}}
|
||||
{{#if field.required}}<span class='required'>*</span>{{/if}}
|
||||
<div class="instructions">{{{field.description}}}</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}}</label>
|
||||
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}} {{#if field.required}}<span class='required'>*</span>{{/if}}
|
||||
</label>
|
||||
<div class='controls'>
|
||||
{{input id=(concat 'user-' elementId) value=value maxlength=site.user_field_max_length}}
|
||||
{{#if field.required}}<span class='required'>*</span>{{/if}}
|
||||
<div class="instructions">{{{field.description}}}</div>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
{{#d-modal-body title="user.auth_tokens.was_this_you"}}
|
||||
<div>
|
||||
<p>{{i18n 'user.auth_tokens.was_this_you_description'}}</p>
|
||||
<p>{{{i18n 'user.second_factor.extended_description'}}}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>{{i18n 'user.auth_tokens.details'}}</h3>
|
||||
<p>{{d-icon "clock-o"}} {{format-date model.seen_at}}</p>
|
||||
<p>{{d-icon "map-marker"}} {{model.location}}</p>
|
||||
<p>{{d-icon model.icon}} {{i18n "user.auth_tokens.browser_and_device" browser=model.browser device=model.device}}</p>
|
||||
</div>
|
||||
|
||||
{{#if latest_post}}
|
||||
<div>
|
||||
<h3>
|
||||
{{i18n 'user.auth_tokens.latest_post'}}
|
||||
<a {{action "toggleExpanded"}}>{{d-icon (if expanded "caret-up" "caret-down")}}</a>
|
||||
</h3>
|
||||
|
||||
{{#if expanded}}
|
||||
<blockquote>{{{latest_post.cooked}}}</blockquote>
|
||||
{{else}}
|
||||
<blockquote>{{{latest_post.excerpt}}}</blockquote>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button class="btn btn-primary" icon="lock" label="user.auth_tokens.secure_account" action="highlightSecure"}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
</div>
|
||||
@ -1,11 +1,8 @@
|
||||
{{#d-modal-body class='change-ownership'}}
|
||||
{{{i18n 'topic.change_owner.instructions' count=selectedPostsCount old_user=selectedPostsUsername}}}
|
||||
<p>
|
||||
{{{i18n 'topic.change_owner.instructions_warn'}}}
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<label>{{i18n 'topic.change_owner.label'}}</label>
|
||||
<label></label>
|
||||
{{user-selector single="true"
|
||||
usernames=new_user
|
||||
placeholderKey="topic.change_owner.placeholder"
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
action="deleteCategory"
|
||||
icon="trash-o"
|
||||
label="category.delete"}}
|
||||
{{else}}
|
||||
{{else if model.id}}
|
||||
<div class="disable_info_wrap">
|
||||
{{d-button disabled=deleteDisabled
|
||||
class="disable-no-hover"
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if showCustomMessage}}
|
||||
<label><span class='optional'>{{i18n 'invite.custom_message'}}</span> <a {{action "showCustomMessageBox"}}>{{i18n 'invite.custom_message_link'}}</a>.</label>
|
||||
<label>{{discourse-linked-text class="optional" action="showCustomMessageBox" text="invite.custom_message"}}</label>
|
||||
{{#if hasCustomMessage}}{{textarea value=customMessage placeholder=customMessagePlaceholder}}{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
<td>
|
||||
{{#if authProvider.method.can_revoke}}
|
||||
{{#conditional-loading-spinner condition=revoking size='small'}}
|
||||
{{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" icon="times-circle" }}
|
||||
{{d-button action="revokeAccount" actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash" }}
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/if}}
|
||||
</td>
|
||||
@ -161,6 +161,48 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if canCheckEmails}}
|
||||
<div class="control-group pref-auth-tokens">
|
||||
<label class="control-label">{{i18n 'user.auth_tokens.title'}}</label>
|
||||
|
||||
<div class="auth-tokens">
|
||||
{{#each authTokens as |token|}}
|
||||
<div class="row auth-token">
|
||||
<div class="auth-token-icon">{{d-icon token.icon}}</div>
|
||||
{{#unless token.is_active}}
|
||||
{{auth-token-dropdown token=token
|
||||
revokeAuthToken=(action "revokeAuthToken")
|
||||
showToken=(action "showToken")}}
|
||||
{{/unless}}
|
||||
<div class="auth-token-first">
|
||||
<span class="auth-token-device">{{token.device}}</span> – <span title="{{i18n "user.auth_tokens.ip"}}: {{token.client_ip}}">{{token.location}}</span>
|
||||
</div>
|
||||
<div class="auth-token-second">
|
||||
{{token.browser}} |
|
||||
{{#if token.is_active}}
|
||||
<span class="active">{{i18n 'user.auth_tokens.active'}}</span>
|
||||
{{else}}
|
||||
{{format-date token.seen_at}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if canShowAllAuthTokens}}
|
||||
<a {{action "toggleShowAllAuthTokens"}}>
|
||||
{{#if showAllAuthTokens}}
|
||||
{{d-icon "caret-up"}} {{i18n 'user.auth_tokens.show_few'}}
|
||||
{{else}}
|
||||
{{d-icon "caret-down"}} {{i18n 'user.auth_tokens.show_all' count=model.user_auth_tokens.length}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<a {{action "revokeAuthToken"}} class="pull-right text-danger">{{d-icon "sign-out"}} {{i18n 'user.auth_tokens.log_out_all'}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
|
||||
|
||||
<br/>
|
||||
|
||||
@ -37,10 +37,10 @@
|
||||
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}}
|
||||
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
|
||||
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}}
|
||||
|
||||
{{#if siteSettings.automatically_unpin_topics}}
|
||||
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
|
||||
{{/if}}
|
||||
{{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence}}
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="user-preferences-interface" args=(hash model=model save=(action "save"))}}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{{tag-chooser
|
||||
tags=model.watched_tags
|
||||
blacklist=selectedTags
|
||||
filterPlaceholder=null
|
||||
filterPlaceholder="select_kit.filter_placeholder"
|
||||
allowCreate=false
|
||||
everyTag=true
|
||||
unlimitedTagCount=true}}
|
||||
@ -20,7 +20,7 @@
|
||||
{{tag-chooser
|
||||
tags=model.tracked_tags
|
||||
blacklist=selectedTags
|
||||
filterPlaceholder=null
|
||||
filterPlaceholder="select_kit.filter_placeholder"
|
||||
allowCreate=false
|
||||
everyTag=true
|
||||
unlimitedTagCount=true}}
|
||||
@ -32,7 +32,7 @@
|
||||
{{tag-chooser
|
||||
tags=model.watching_first_post_tags
|
||||
blacklist=selectedTags
|
||||
filterPlaceholder=null
|
||||
filterPlaceholder="select_kit.filter_placeholder"
|
||||
allowCreate=false
|
||||
everyTag=true
|
||||
unlimitedTagCount=true}}
|
||||
@ -44,7 +44,7 @@
|
||||
{{tag-chooser
|
||||
tags=model.muted_tags
|
||||
blacklist=selectedTags
|
||||
filterPlaceholder=null
|
||||
filterPlaceholder="select_kit.filter_placeholder"
|
||||
allowCreate=false
|
||||
everyTag=true
|
||||
unlimitedTagCount=true}}
|
||||
|
||||
@ -195,8 +195,10 @@
|
||||
</section>
|
||||
|
||||
{{#mobile-nav class='main-nav' desktopClass="nav nav-pills user-nav" currentPath=currentPath}}
|
||||
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
|
||||
{{#unless model.profile_hidden}}
|
||||
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
|
||||
{{/unless}}
|
||||
{{#if showNotificationsTab}}
|
||||
<li>
|
||||
{{#link-to 'userNotifications'}}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<p class='user-profile-hidden'>{{i18n "user.profile_hidden"}}</p>
|
||||
@ -53,7 +53,7 @@ export default createWidget("hamburger-categories", {
|
||||
const href = Discourse.getURL("/categories");
|
||||
let title = I18n.t("filters.categories.title");
|
||||
if (attrs.moreCount > 0) {
|
||||
title += I18n.t("categories.more", { count: attrs.moreCount });
|
||||
title = I18n.t("categories.n_more", { count: attrs.moreCount });
|
||||
}
|
||||
|
||||
let result = [
|
||||
|
||||
@ -81,7 +81,7 @@ export default createWidget("post-small-action", {
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: "small-action-delete",
|
||||
icon: "times",
|
||||
icon: "trash",
|
||||
action: "deletePost",
|
||||
title: "post.controls.delete"
|
||||
})
|
||||
|
||||
@ -184,8 +184,8 @@ export default createWidget("topic-admin-menu", {
|
||||
|
||||
const isPrivateMessage = topic.get("isPrivateMessage");
|
||||
|
||||
if (!isPrivateMessage && topic.get("visible")) {
|
||||
const featured = topic.get("pinned_at") || topic.get("isBanner");
|
||||
const featured = topic.get("pinned_at") || topic.get("isBanner");
|
||||
if (!isPrivateMessage && (topic.get("visible") || featured)) {
|
||||
buttons.push({
|
||||
className: "topic-admin-pin",
|
||||
action: "showFeatureTopic",
|
||||
|
||||
17
app/assets/javascripts/google-tag-manager.js.no-module.es6
Normal file
17
app/assets/javascripts/google-tag-manager.js.no-module.es6
Normal file
@ -0,0 +1,17 @@
|
||||
(function() {
|
||||
const gtmDataElement = document.getElementById("data-google-tag-manager");
|
||||
const dataLayerJson = JSON.parse(gtmDataElement.dataset.dataLayer);
|
||||
|
||||
// dataLayer declaration needs to precede the container snippet
|
||||
// https://developers.google.com/tag-manager/devguide#adding-data-layer-variables-to-a-page
|
||||
window.dataLayer = [dataLayerJson];
|
||||
|
||||
/* eslint-disable */
|
||||
// prettier-ignore
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer',gtmDataElement.dataset.containerId);
|
||||
/* eslint-enable */
|
||||
})();
|
||||
@ -0,0 +1,20 @@
|
||||
/* eslint-disable */
|
||||
// prettier-ignore
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
/* eslint-enable */
|
||||
|
||||
(function() {
|
||||
const gaDataElement = document.getElementById("data-ga-universal-analytics");
|
||||
const gaJson = JSON.parse(gaDataElement.dataset.json);
|
||||
|
||||
window.ga("create", gaDataElement.dataset.trackingCode, gaJson);
|
||||
if (gaDataElement.dataset.autoLinkDomains.length) {
|
||||
const autoLinkDomains = gaDataElement.dataset.autoLinkDomains.split("|");
|
||||
|
||||
window.ga("require", "linker");
|
||||
window.ga("linker:autoLink", autoLinkDomains);
|
||||
}
|
||||
})();
|
||||
3
app/assets/javascripts/locales/lt.js.erb
Normal file
3
app/assets/javascripts/locales/lt.js.erb
Normal file
@ -0,0 +1,3 @@
|
||||
//= depend_on 'client.lt.yml'
|
||||
//= require locales/i18n
|
||||
<%= JsLocaleHelper.output_locale(:lt) %>
|
||||
@ -0,0 +1,64 @@
|
||||
(function() {
|
||||
var ps = require("preload-store").default;
|
||||
var preloadedDataElement = document.getElementById("data-preloaded");
|
||||
|
||||
if (preloadedDataElement) {
|
||||
var preloaded = JSON.parse(preloadedDataElement.dataset.preloaded);
|
||||
|
||||
Object.keys(preloaded).forEach(function(key) {
|
||||
ps.store(key, JSON.parse(preloaded[key]));
|
||||
});
|
||||
}
|
||||
|
||||
var setupData = document.getElementById("data-discourse-setup").dataset;
|
||||
|
||||
Discourse.CDN = setupData.cdn;
|
||||
Discourse.BaseUrl = setupData.baseUrl;
|
||||
Discourse.BaseUri = setupData.baseUri;
|
||||
Discourse.Environment = setupData.environment;
|
||||
Discourse.SiteSettings = ps.get("siteSettings");
|
||||
Discourse.ThemeSettings = ps.get("themeSettings");
|
||||
Discourse.LetterAvatarVersion = setupData.letterAvatarVersion;
|
||||
Discourse.MarkdownItURL = setupData.markdownItUrl;
|
||||
Discourse.ServiceWorkerURL = setupData.serviceWorkerUrl;
|
||||
I18n.defaultLocale = setupData.defaultLocale;
|
||||
Discourse.start();
|
||||
Discourse.set("assetVersion", setupData.assetVersion);
|
||||
Discourse.Session.currentProp(
|
||||
"disableCustomCSS",
|
||||
setupData.disableCustomCss === "true"
|
||||
);
|
||||
|
||||
if (setupData.safeMode) {
|
||||
Discourse.Session.currentProp("safe_mode", setupData.safeMode);
|
||||
}
|
||||
|
||||
Discourse.HighlightJSPath = setupData.highlightJsPath;
|
||||
|
||||
if (setupData.s3BaseUrl) {
|
||||
Discourse.S3CDN = setupData.s3Cdn;
|
||||
Discourse.S3BaseUrl = setupData.s3BaseUrl;
|
||||
}
|
||||
|
||||
Ember.RSVP.configure("onerror", function(e) {
|
||||
// Ignore TransitionAborted exceptions that bubble up
|
||||
if (e && e.message === "TransitionAborted") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Discourse.Environment === "development") {
|
||||
if (e) {
|
||||
if (e.message || e.stack) {
|
||||
console.log(e.message);
|
||||
console.log(e.stack);
|
||||
} else {
|
||||
console.log("Uncaught promise: ", e);
|
||||
}
|
||||
} else {
|
||||
console.log("A promise failed but was not caught.");
|
||||
}
|
||||
}
|
||||
|
||||
window.onerror(e && e.message, null, null, null, e);
|
||||
});
|
||||
})();
|
||||
@ -2,6 +2,10 @@ function escapeRegexp(text) {
|
||||
return text.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&").replace(/\*/g, "S*");
|
||||
}
|
||||
|
||||
function createCensorRegexp(patterns) {
|
||||
return new RegExp(`((?<!\\w)(?:${patterns.join("|")}))(?!\\w)`, "ig");
|
||||
}
|
||||
|
||||
export function censorFn(
|
||||
censoredWords,
|
||||
replacementLetter,
|
||||
@ -28,10 +32,7 @@ export function censorFn(
|
||||
"ig"
|
||||
);
|
||||
} else {
|
||||
censorRegexp = new RegExp(
|
||||
"(\\b(?:" + patterns.join("|") + ")\\b)(?![^\\(]*\\))",
|
||||
"ig"
|
||||
);
|
||||
censorRegexp = createCensorRegexp(patterns);
|
||||
}
|
||||
|
||||
if (censorRegexp) {
|
||||
@ -53,10 +54,7 @@ export function censorFn(
|
||||
replacementLetter
|
||||
);
|
||||
text = text.replace(
|
||||
new RegExp(
|
||||
`(\\b${escapeRegexp(m[0])}\\b)(?![^\\(]*\\))`,
|
||||
"ig"
|
||||
),
|
||||
createCensorRegexp([escapeRegexp(m[0])]),
|
||||
replacement
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
const { makeArray } = Ember;
|
||||
|
||||
export default MultiSelectComponent.extend({
|
||||
@ -7,11 +8,13 @@ export default MultiSelectComponent.extend({
|
||||
selected: null,
|
||||
available: null,
|
||||
allowAny: false,
|
||||
buffer: null,
|
||||
|
||||
computeValues() {
|
||||
return makeArray(this.get("selected")).map(s =>
|
||||
this.valueForContentItem(s)
|
||||
);
|
||||
@computed("buffer")
|
||||
values(buffer) {
|
||||
return buffer === null
|
||||
? makeArray(this.get("selected")).map(s => this.valueForContentItem(s))
|
||||
: buffer;
|
||||
},
|
||||
|
||||
computeContent() {
|
||||
@ -25,33 +28,6 @@ export default MultiSelectComponent.extend({
|
||||
},
|
||||
|
||||
mutateValues(values) {
|
||||
if (values.length > this.get("selected").length) {
|
||||
const newValues = values.filter(
|
||||
v =>
|
||||
!this.get("selected")
|
||||
.map(s => this.valueForContentItem(s))
|
||||
.includes(v)
|
||||
);
|
||||
|
||||
newValues.forEach(value => {
|
||||
const actionContext = this.get("available").findBy(
|
||||
this.get("valueAttribute"),
|
||||
parseInt(value, 10)
|
||||
);
|
||||
|
||||
this.triggerAction({ action: "groupAdded", actionContext });
|
||||
});
|
||||
} else if (values.length < this.get("selected").length) {
|
||||
const selected = this.get("selected").filter(
|
||||
s => !values.includes(this.valueForContentItem(s))
|
||||
);
|
||||
|
||||
selected.forEach(s => {
|
||||
this.triggerAction({
|
||||
action: "groupRemoved",
|
||||
actionContext: this.valueForContentItem(s)
|
||||
});
|
||||
});
|
||||
}
|
||||
this.set("buffer", values);
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@ export default DropdownSelectBoxComponent.extend({
|
||||
classNames: "categories-admin-dropdown",
|
||||
showFullTitle: false,
|
||||
allowInitialValueMutation: false,
|
||||
headerIcon: ["bars", "caret-down"],
|
||||
headerIcon: ["bars"],
|
||||
|
||||
autoHighlight() {},
|
||||
|
||||
|
||||
@ -42,16 +42,13 @@ export default ComboBoxSelectBoxHeaderComponent.extend({
|
||||
if (categoryBackgroundColor || categoryTextColor) {
|
||||
let style = "";
|
||||
if (categoryBackgroundColor) {
|
||||
if (categoryStyle === "bar") {
|
||||
style += `border-color: #${categoryBackgroundColor};`;
|
||||
} else if (categoryStyle === "box") {
|
||||
style += `background-color: #${categoryBackgroundColor};`;
|
||||
if (categoryStyle === "box") {
|
||||
style += `border-color: #${categoryBackgroundColor}; background-color: #${categoryBackgroundColor};`;
|
||||
if (categoryTextColor) {
|
||||
style += `color: #${categoryTextColor};`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style.htmlSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,5 +11,7 @@ export default NotificationOptionsComponent.extend({
|
||||
|
||||
mutateValue(value) {
|
||||
this.get("category").setNotification(value);
|
||||
}
|
||||
},
|
||||
|
||||
deselect() {}
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-bo
|
||||
|
||||
export default DropdownSelectBoxComponent.extend({
|
||||
classNames: "group-members-dropdown",
|
||||
headerIcon: ["bars", "caret-down"],
|
||||
headerIcon: ["bars"],
|
||||
showFullTitle: false,
|
||||
allowInitialValueMutation: false,
|
||||
autoHighlight() {},
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
import Tags from "select-kit/mixins/tags";
|
||||
import TagsMixin from "select-kit/mixins/tags";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import renderTag from "discourse/lib/render-tag";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
const { get, isEmpty, run, makeArray } = Ember;
|
||||
|
||||
export default ComboBox.extend(Tags, {
|
||||
export default ComboBox.extend(TagsMixin, {
|
||||
allowContentReplacement: true,
|
||||
headerComponent: "mini-tag-chooser/mini-tag-chooser-header",
|
||||
pluginApiIdentifiers: ["mini-tag-chooser"],
|
||||
@ -220,6 +220,28 @@ export default ComboBox.extend(Tags, {
|
||||
this.destroyTags(tags);
|
||||
},
|
||||
|
||||
_sanitizeContent(content, property) {
|
||||
switch (typeof content) {
|
||||
case "string":
|
||||
// See lib/discourse_tagging#clean_tag.
|
||||
return content
|
||||
.trim()
|
||||
.replace(/\s+/, "-")
|
||||
.replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "")
|
||||
.substring(0, this.siteSettings.max_tag_length);
|
||||
default:
|
||||
return get(content, this.get(property));
|
||||
}
|
||||
},
|
||||
|
||||
valueForContentItem(content) {
|
||||
return this._sanitizeContent(content, "valueAttribute");
|
||||
},
|
||||
|
||||
_nameForContent(content) {
|
||||
return this._sanitizeContent(content, "nameProperty");
|
||||
},
|
||||
|
||||
actions: {
|
||||
onSelect(tag) {
|
||||
this.set("tags", makeArray(this.get("tags")).concat(tag));
|
||||
|
||||
@ -39,7 +39,7 @@ export default SelectKitComponent.extend({
|
||||
@on("didRender")
|
||||
_setChoicesMaxWidth() {
|
||||
const width = this.$body().outerWidth(false);
|
||||
this.$(".choices").css({ maxWidth: width, width });
|
||||
this.$(".choices").css({ maxWidth: width });
|
||||
},
|
||||
|
||||
@on("didReceiveAttrs")
|
||||
|
||||
@ -210,6 +210,10 @@ export default SelectKitComponent.extend({
|
||||
},
|
||||
|
||||
select(computedContentItem) {
|
||||
if (this.get("hasSelection")) {
|
||||
this.deselect(this.get("selection.value"));
|
||||
}
|
||||
|
||||
if (
|
||||
!computedContentItem ||
|
||||
computedContentItem.__sk_row_type === "noneRow"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user