Version bump

This commit is contained in:
Neil Lalonde 2018-10-12 10:56:02 -04:00
commit df61b9309d
970 changed files with 18441 additions and 13710 deletions

2
.gitignore vendored
View File

@ -122,3 +122,5 @@ vendor/bundle/*
# ignore nodejs files
/node_modules
/package-lock.json
/vendor/data/GeoLite2-City.mmdb

View File

@ -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

View File

@ -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

View File

@ -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")}

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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());

View File

@ -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");

View 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");
}
}
});

View File

@ -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);
}
}
});

View File

@ -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() {

View File

@ -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();
}
}
}
});

View File

@ -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"));
}

View File

@ -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() {

View File

@ -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")

View File

@ -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));
}
}
});

View File

@ -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")) {

View File

@ -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() {

View File

@ -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}`;
},

View File

@ -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();
}
}
);

View File

@ -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();
}
}
);
}
}
}
});

View File

@ -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")
});
}
});

View File

@ -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);
}
},

View File

@ -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}}

View File

@ -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"

View File

@ -1,5 +1,5 @@
<label>
{{input type="checkbox" checked=enabled}}
{{{unbound setting.description}}}
<span>{{{unbound setting.description}}}</span>
{{setting-validation-message message=validationMessage}}
</label>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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">

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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

View File

@ -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();
}
}
});
})();

View File

@ -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, "&#39;")}'`;
}
if (params.label) {
html += " aria-hidden='true'";

View File

@ -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;
}
}
}
});

View File

@ -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)

View File

@ -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;

View File

@ -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>");
}
}
})
);

View File

@ -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"));

View File

@ -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}`);
}
});

View File

@ -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");
}
},

View File

@ -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"
});
});
}
}
});

View File

@ -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();
}

View File

@ -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) {

View File

@ -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;
},

View File

@ -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);

View File

@ -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);
}
});
}

View File

@ -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",

View File

@ -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 +
">";

View File

@ -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) {

View File

@ -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) {

View File

@ -102,6 +102,7 @@ export default function() {
"user",
{ path: "/u/:username", resetNamespace: true },
function() {
this.route("profile-hidden");
this.route("summary");
this.route(
"userActivity",

View File

@ -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")

View File

@ -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);

View File

@ -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;
}
});

View File

@ -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"),

View File

@ -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) {

View File

@ -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: {

View File

@ -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}}

View File

@ -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

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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}}

View File

@ -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> &ndash; <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/>

View File

@ -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"))}}

View File

@ -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}}

View File

@ -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'}}

View File

@ -0,0 +1 @@
<p class='user-profile-hidden'>{{i18n "user.profile_hidden"}}</p>

View File

@ -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 = [

View File

@ -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"
})

View File

@ -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",

View 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 */
})();

View File

@ -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);
}
})();

View File

@ -0,0 +1,3 @@
//= depend_on 'client.lt.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:lt) %>

View File

@ -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);
});
})();

View File

@ -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
);
}

View File

@ -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);
}
});

View File

@ -5,7 +5,7 @@ export default DropdownSelectBoxComponent.extend({
classNames: "categories-admin-dropdown",
showFullTitle: false,
allowInitialValueMutation: false,
headerIcon: ["bars", "caret-down"],
headerIcon: ["bars"],
autoHighlight() {},

View File

@ -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();
}
}

View File

@ -11,5 +11,7 @@ export default NotificationOptionsComponent.extend({
mutateValue(value) {
this.get("category").setNotification(value);
}
},
deselect() {}
});

View File

@ -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() {},

View File

@ -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));

View File

@ -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")

View File

@ -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