Version bump

This commit is contained in:
Neil Lalonde 2018-05-31 18:29:51 -04:00
commit a2ebae2d5b
453 changed files with 19465 additions and 9848 deletions

View File

@ -1,9 +1,15 @@
language: ruby
branches:
only:
- master
- beta
- stable
env:
global:
- DISCOURSE_HOSTNAME=www.example.com
- RUBY_GC_MALLOC_LIMIT=50000000
- RUBY_GLOBAL_METHOD_CACHE_SIZE=131072
matrix:
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0"
- "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0"
@ -11,8 +17,9 @@ env:
addons:
chrome: stable
postgresql: 9.5
postgresql: 9.6
apt:
update: true
packages:
- gifsicle
- jpegoptim
@ -33,6 +40,7 @@ sudo: required
dist: trusty
cache:
apt: true
yarn: true
directories:
- vendor/bundle
@ -50,10 +58,11 @@ before_install:
- export PATH=$HOME/.yarn/bin:$PATH
install:
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
- bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint; fi"
- bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev; fi"
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu > /dev/null; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3 > /dev/null; fi"
- bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint > /dev/null; fi"
- bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev > /dev/null; fi"
- bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create db:migrate > /dev/null; fi"
script:
- |
@ -66,8 +75,6 @@ script:
eslint --ext .es6 plugins/**/test/javascripts && \
eslint app/assets/javascripts test/javascripts
else
bundle exec rake db:create db:migrate
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['500000'] && \
bundle exec rake plugin:qunit

View File

@ -34,7 +34,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.47'
gem 'onebox', '1.8.48'
gem 'http_accept_language', '~>2.0.5', require: false
@ -58,7 +58,7 @@ gem 'aws-sdk-s3', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '0.1.11'
gem 'email_reply_trimmer', '~> 0.1'
# Forked until https://github.com/toy/image_optim/pull/149 is merged
gem 'discourse_image_optim', require: 'image_optim'
@ -115,7 +115,7 @@ group :test, :development do
gem 'listen', require: false
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
gem 'fabrication', '2.9.8', require: false
gem 'fabrication', require: false
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false

View File

@ -89,7 +89,7 @@ GEM
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
email_reply_trimmer (0.1.11)
email_reply_trimmer (0.1.12)
ember-data-source (2.2.1)
ember-source (>= 1.8, < 3.0)
ember-handlebars-template (0.7.5)
@ -225,7 +225,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.47)
onebox (1.8.48)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@ -415,13 +415,13 @@ DEPENDENCIES
certified
cppjieba_rb
discourse_image_optim
email_reply_trimmer (= 0.1.11)
email_reply_trimmer (~> 0.1)
ember-handlebars-template (= 0.7.5)
ember-rails (= 0.18.5)
ember-source (= 2.13.3)
excon
execjs
fabrication (= 2.9.8)
fabrication
fakeweb (~> 1.3.0)
fast_blank
fast_xor
@ -461,7 +461,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.47)
onebox (= 1.8.48)
openid-redis-store
pg (~> 0.21.0)
pry-nav

View File

@ -1,4 +1,5 @@
import loadScript from 'discourse/lib/load-script';
import { number } from 'discourse/lib/formatter';
export default Ember.Component.extend({
tagName: 'canvas',
@ -22,10 +23,16 @@ export default Ember.Component.extend({
data: data,
options: {
responsive: true,
tooltips: {
callbacks: {
title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
scales: {
yAxes: [{
display: true,
ticks: {
callback: (label) => number(label),
suggestedMin: 0
}
}]

View File

@ -1,32 +1,13 @@
import { ajax } from "discourse/lib/ajax";
import Report from "admin/models/report";
import AsyncReport from "admin/mixins/async-report";
export default Ember.Component.extend(AsyncReport, {
classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
help: null,
helpPage: null,
loadReport(report_json) {
return Report.create(report_json);
},
classNames: ["dashboard-inline-table"],
fetchReport() {
this._super();
let payload = { data: { cache: true, facets: ["total", "prev30Days"] } };
if (this.get("startDate")) {
payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
}
if (this.get("endDate")) {
payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
}
if (this.get("limit")) {
payload.data.limit = this.get("limit");
}
let payload = this.buildPayload(["total", "prev30Days"]);
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
return ajax(dataSource, payload)

View File

@ -52,17 +52,7 @@ export default Ember.Component.extend(AsyncReport, {
fetchReport() {
this._super();
let payload = {
data: { cache: true, facets: ["prev_period"] }
};
if (this.get("startDate")) {
payload.data.start_date = this.get("startDate").locale('en').format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ');
}
if (this.get("endDate")) {
payload.data.end_date = this.get("endDate").locale('en').format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ');
}
let payload = this.buildPayload(["prev_period"]);
if (this._chart) {
this._chart.destroy();
@ -110,7 +100,7 @@ export default Ember.Component.extend(AsyncReport, {
labels,
datasets: reportsForPeriod.map(report => {
return {
data: Ember.makeArray(report.data).map(d => d.y),
data: Ember.makeArray(report.data).map(d => number(d.y, { ceil: true })),
backgroundColor: "rgba(200,220,240,0.3)",
borderColor: report.color
};
@ -157,7 +147,7 @@ export default Ember.Component.extend(AsyncReport, {
scales: {
yAxes: [{
display: true,
ticks: { callback: (label) => number(label) }
ticks: { callback: (label) => number(label, { ceil: true }) }
}],
xAxes: [{
display: true,

View File

@ -0,0 +1,19 @@
import { ajax } from "discourse/lib/ajax";
import AsyncReport from "admin/mixins/async-report";
export default Ember.Component.extend(AsyncReport, {
classNames: ["dashboard-table"],
fetchReport() {
this._super();
let payload = this.buildPayload(["total", "prev30Days"]);
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
return ajax(dataSource, payload)
.then(response => {
this.get("reports").pushObject(this.loadReport(response.report));
});
}));
}
});

View File

@ -5,7 +5,7 @@ export default Ember.Component.extend({
classNames: ['watched-word-form'],
formSubmitted: false,
actionKey: null,
showSuccessMessage: false,
showMessage: false,
@computed('regularExpressions')
placeholderKey(regularExpressions) {
@ -14,21 +14,33 @@ export default Ember.Component.extend({
},
@observes('word')
removeSuccessMessage() {
if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) {
this.set('showSuccessMessage', false);
removeMessage() {
if (this.get('showMessage') && !Ember.isEmpty(this.get('word'))) {
this.set('showMessage', false);
}
},
@computed('word')
isUniqueWord(word) {
const words = this.get("filteredContent") || [];
const filtered = words.filter(content => content.action === this.get("actionKey"));
return filtered.every(content => content.word.toLowerCase() !== word.toLowerCase());
},
actions: {
submit() {
if (!this.get("isUniqueWord")) {
this.setProperties({ showMessage: true, message: I18n.t('admin.watched_words.form.exists') });
return;
}
if (!this.get('formSubmitted')) {
this.set('formSubmitted', true);
const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') });
watchedWord.save().then(result => {
this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true });
this.setProperties({ word: '', formSubmitted: false, showMessage: true, message: I18n.t('admin.watched_words.form.success') });
this.sendAction('action', WatchedWord.create(result));
Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus());
}).catch(e => {

View File

@ -4,6 +4,7 @@ import Report from 'admin/models/report';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
classNames: ["admin-reports"],
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
viewMode: 'graph',
viewingTable: Em.computed.equal('viewMode', 'table'),

View File

@ -50,6 +50,36 @@ export default Ember.Controller.extend(CanCheckEmails, {
return userPath(`${username}/preferences`);
},
@computed('model.can_delete_all_posts', 'model.staff', 'model.post_count')
deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) {
if (canDeleteAllPosts) {
return null;
}
if (staff) {
return I18n.t('admin.user.delete_posts_forbidden_because_staff');
}
if (postCount > this.siteSettings.delete_all_posts_max) {
return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: this.siteSettings.delete_all_posts_max});
} else {
return I18n.t('admin.user.cant_delete_all_posts', {count: this.siteSettings.delete_user_max_post_age});
}
},
@computed('model.canBeDeleted', 'model.staff')
deleteExplanation(canBeDeleted, staff) {
if (canBeDeleted) {
return null;
}
if (staff) {
return I18n.t('admin.user.delete_forbidden_because_staff');
} else {
return I18n.t('admin.user.delete_forbidden', {count: this.siteSettings.delete_user_max_post_age});
}
},
actions: {
impersonate() { return this.get("model").impersonate(); },
@ -73,6 +103,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
clearPenaltyHistory() {
let user = this.get('model');
return ajax(`/admin/users/${user.get('id')}/penalty_history`, {
type: 'DELETE'
}).then(() => {
user.set('tl3_requirements.penalty_counts.total', 0);
}).catch(popupAjaxError);
},
destroy() {
const postCount = this.get('model.post_count');
if (postCount <= 5) {

View File

@ -46,6 +46,10 @@ export default Ember.Controller.extend({
actions: {
clearFilter() {
this.setProperties({ filter: '' });
},
toggleMenu() {
$('.admin-detail').toggleClass('mobile-closed mobile-open');
}
}

View File

@ -1,7 +1,8 @@
import computed from "ember-addons/ember-computed-decorators";
import Report from "admin/models/report";
export default Ember.Mixin.create({
classNameBindings: ["isLoading"],
classNameBindings: ["isLoading", "dataSourceNames"],
reports: null,
isLoading: false,
dataSourceNames: "",
@ -17,6 +18,24 @@ export default Ember.Mixin.create({
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
},
buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.get("startDate")) {
payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
}
if (this.get("endDate")) {
payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
}
if (this.get("limit")) {
payload.data.limit = this.get("limit");
}
return payload;
},
@computed("reports.[]", "startDate", "endDate", "dataSourceNames")
reportsForPeriod(reports, startDate, endDate, dataSourceNames) {
// on a slow network fetchReport could be called multiple times between
@ -25,7 +44,6 @@ export default Ember.Mixin.create({
// the array contains only unique values
reports = reports.uniqBy("report_key");
const sort = (r) => {
if (r.length > 1) {
return dataSourceNames
@ -40,7 +58,6 @@ export default Ember.Mixin.create({
return sort(reports);
}
return sort(reports.filter(report => {
return report.report_key.includes(startDate.format("YYYYMMDD")) &&
report.report_key.includes(endDate.format("YYYYMMDD"));
@ -71,7 +88,9 @@ export default Ember.Mixin.create({
this.set("isLoading", false);
},
loadReport() {},
loadReport(jsonReport) {
return Report.create(jsonReport);
},
fetchReport() {
this.set("reports", []);

View File

@ -87,21 +87,6 @@ const AdminUser = Discourse.User.extend({
}).then(() => this.set('api_key', null));
},
deleteAllPostsExplanation: function() {
if (!this.get('can_delete_all_posts')) {
if (this.get('deleteForbidden') && this.get('staff')) {
return I18n.t('admin.user.delete_posts_forbidden_because_staff');
}
if (this.get('post_count') > Discourse.SiteSettings.delete_all_posts_max) {
return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: Discourse.SiteSettings.delete_all_posts_max});
} else {
return I18n.t('admin.user.cant_delete_all_posts', {count: Discourse.SiteSettings.delete_user_max_post_age});
}
} else {
return null;
}
}.property('can_delete_all_posts', 'deleteForbidden'),
deleteAllPosts() {
const user = this,
message = I18n.messageFormat('admin.user.delete_all_posts_confirm_MF', { "POSTS": user.get('post_count'), "TOPICS": user.get('topic_count') }),
@ -240,8 +225,6 @@ const AdminUser = Discourse.User.extend({
return this.get('trust_level') < 4;
}.property('trust_level'),
isSuspended: Em.computed.equal('suspended', true),
isSilenced: Ember.computed.equal('silenced', true),
canSuspend: Em.computed.not('staff'),
suspendDuration: function() {
@ -353,8 +336,6 @@ const AdminUser = Discourse.User.extend({
}).catch(popupAjaxError);
},
anonymizeForbidden: Em.computed.not("can_be_anonymized"),
anonymize() {
const user = this,
message = I18n.t("admin.user.anonymize_confirm");
@ -393,20 +374,6 @@ const AdminUser = Discourse.User.extend({
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
deleteForbidden: Em.computed.not("canBeDeleted"),
deleteExplanation: function() {
if (this.get('deleteForbidden')) {
if (this.get('staff')) {
return I18n.t('admin.user.delete_forbidden_because_staff');
} else {
return I18n.t('admin.user.delete_forbidden', {count: Discourse.SiteSettings.delete_user_max_post_age});
}
} else {
return null;
}
}.property('deleteForbidden'),
destroy(opts) {
const user = this,
message = I18n.t("admin.user.delete_confirm"),

View File

@ -1,22 +1,24 @@
import { ajax } from 'discourse/lib/ajax';
import { ajax } from "discourse/lib/ajax";
import round from "discourse/lib/round";
import { fillMissingDates } from 'discourse/lib/utilities';
import computed from 'ember-addons/ember-computed-decorators';
import { fillMissingDates } from "discourse/lib/utilities";
import computed from "ember-addons/ember-computed-decorators";
import { number } from 'discourse/lib/formatter';
const Report = Discourse.Model.extend({
average: false,
percent: false,
higher_is_better: true,
@computed("type", "start_date", "end_date")
reportUrl(type, start_date, end_date) {
start_date = moment(start_date).locale('en').format("YYYY-MM-DD");
end_date = moment(end_date).locale('en').format("YYYY-MM-DD");
start_date = moment(start_date).locale("en").format("YYYY-MM-DD");
end_date = moment(end_date).locale("en").format("YYYY-MM-DD");
return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`);
},
valueAt(numDaysAgo) {
if (this.data) {
const wantedDate = moment().subtract(numDaysAgo, "days").locale('en').format("YYYY-MM-DD");
const wantedDate = moment().subtract(numDaysAgo, "days").locale("en").format("YYYY-MM-DD");
const item = this.data.find(d => d.x === wantedDate);
if (item) {
return item.y;
@ -29,7 +31,7 @@ const Report = Discourse.Model.extend({
if (this.data) {
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
var d, sum = 0, count = 0;
let d, sum = 0, count = 0;
_.each(this.data, datum => {
d = moment(datum.x);
if (d >= earliestDate && d <= latestDate) {
@ -46,6 +48,7 @@ const Report = Discourse.Model.extend({
yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"),
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"),
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"),
lastSevenDaysCount: function() {
return this.averageCount(7, this.valueFor(1, 7));
}.property("data", "average"),
@ -57,112 +60,66 @@ const Report = Discourse.Model.extend({
return this.get("average") ? value / count : value;
},
@computed('yesterdayCount')
yesterdayTrend(yesterdayCount) {
const yesterdayVal = yesterdayCount;
const twoDaysAgoVal = this.valueAt(2);
const change = ((yesterdayVal - twoDaysAgoVal) / yesterdayVal) * 100;
if (change > 50) {
return "high-trending-up";
} else if (change > 0) {
return "trending-up";
} else if (change === 0) {
return "no-change";
} else if (change < -50) {
return "high-trending-down";
} else if (change < 0) {
return "trending-down";
}
@computed("yesterdayCount", "higher_is_better")
yesterdayTrend(yesterdayCount, higherIsBetter) {
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
},
@computed('lastSevenDaysCount')
sevenDayTrend(lastSevenDaysCount) {
const currentPeriod = lastSevenDaysCount;
const prevPeriod = this.valueFor(8, 14);
const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
if (change > 50) {
return "high-trending-up";
} else if (change > 0) {
return "trending-up";
} else if (change === 0) {
return "no-change";
} else if (change < -50) {
return "high-trending-down";
} else if (change < 0) {
return "trending-down";
}
@computed("lastSevenDaysCount", "higher_is_better")
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
return this._computeTrend(this.valueFor(8, 14), lastSevenDaysCount, higherIsBetter);
},
@computed('data')
@computed("data")
currentTotal(data){
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
},
@computed('data', 'currentTotal')
@computed("data", "currentTotal")
currentAverage(data, total) {
return Ember.makeArray(data).length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1));
},
@computed("trend")
trendIcon(trend) {
switch (trend) {
case "trending-up":
return "angle-up";
case "trending-down":
return "angle-down";
case "high-trending-up":
return "angle-double-up";
case "high-trending-down":
return "angle-double-down";
default:
return null;
}
@computed("trend", "higher_is_better")
trendIcon(trend, higherIsBetter) {
return this._iconForTrend(trend, higherIsBetter);
},
@computed('prev_period', 'currentTotal', 'currentAverage')
trend(prev, currentTotal, currentAverage) {
const total = this.get('average') ? currentAverage : currentTotal;
const change = ((total - prev) / total) * 100;
if (change > 50) {
return "high-trending-up";
} else if (change > 0) {
return "trending-up";
} else if (change === 0) {
return "no-change";
} else if (change < -50) {
return "high-trending-down";
} else if (change < 0) {
return "trending-down";
}
@computed("sevenDaysTrend", "higher_is_better")
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
},
@computed('prev30Days', 'lastThirtyDaysCount')
thirtyDayTrend(prev30Days, lastThirtyDaysCount) {
const currentPeriod = lastThirtyDaysCount;
const change = ((currentPeriod - prev30Days) / currentPeriod) * 100;
if (change > 50) {
return "high-trending-up";
} else if (change > 0) {
return "trending-up";
} else if (change === 0) {
return "no-change";
} else if (change < -50) {
return "high-trending-down";
} else if (change < 0) {
return "trending-down";
}
@computed("thirtyDaysTrend", "higher_is_better")
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
},
@computed('type')
@computed("yesterdayTrend", "higher_is_better")
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
return this._iconForTrend(yesterdayTrend, higherIsBetter);
},
@computed("prev_period", "currentTotal", "currentAverage", "higher_is_better")
trend(prev, currentTotal, currentAverage, higherIsBetter) {
const total = this.get("average") ? currentAverage : currentTotal;
return this._computeTrend(prev, total, higherIsBetter);
},
@computed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
},
@computed("type")
icon(type) {
if (type.indexOf("message") > -1) {
return "envelope";
}
switch (type) {
case "page_view_total_reqs": return "file";
case "visits": return "user";
case "time_to_first_response": return "reply";
case "flags": return "flag";
case "likes": return "heart";
case "bookmarks": return "bookmark";
@ -170,7 +127,7 @@ const Report = Discourse.Model.extend({
}
},
@computed('type')
@computed("type")
method(type) {
if (type === "time_to_first_response") {
return "average";
@ -180,75 +137,112 @@ const Report = Discourse.Model.extend({
},
percentChangeString(val1, val2) {
const val = ((val1 - val2) / val2) * 100;
if (isNaN(val) || !isFinite(val)) {
const change = this._computeChange(val1, val2);
if (isNaN(change) || !isFinite(change)) {
return null;
} else if (val > 0) {
return "+" + val.toFixed(0) + "%";
} else if (change > 0) {
return "+" + change.toFixed(0) + "%";
} else {
return val.toFixed(0) + "%";
return change.toFixed(0) + "%";
}
},
@computed('prev_period', 'currentTotal', 'currentAverage')
@computed("prev_period", "currentTotal", "currentAverage")
trendTitle(prev, currentTotal, currentAverage) {
let current = this.get('average') ? currentAverage : currentTotal;
let percent = this.percentChangeString(current, prev);
let current = this.get("average") ? currentAverage : currentTotal;
let percent = this.percentChangeString(prev, current);
if (this.get('average')) {
if (this.get("average")) {
prev = prev ? prev.toFixed(1) : "0";
if (this.get('percent')) {
current += '%';
prev += '%';
if (this.get("percent")) {
current += "%";
prev += "%";
}
} else {
prev = number(prev);
current = number(current);
}
return I18n.t('admin.dashboard.reports.trend_title', {percent: percent, prev: prev, current: current});
return I18n.t("admin.dashboard.reports.trend_title", {percent, prev, current});
},
changeTitle(val1, val2, prevPeriodString) {
const percentChange = this.percentChangeString(val1, val2);
var title = "";
if (percentChange) { title += percentChange + " change. "; }
title += "Was " + val2 + " " + prevPeriodString + ".";
changeTitle(valAtT1, valAtT2, prevPeriodString) {
const change = this.percentChangeString(valAtT1, valAtT2);
let title = "";
if (change) { title += `${change} change. `; }
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
return title;
},
@computed('yesterdayCount')
@computed("yesterdayCount")
yesterdayCountTitle(yesterdayCount) {
return this.changeTitle(yesterdayCount, this.valueAt(2), "two days ago");
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
},
@computed('lastSevenDaysCount')
sevenDayCountTitle(lastSevenDaysCount) {
return this.changeTitle(lastSevenDaysCount, this.valueFor(8, 14), "two weeks ago");
@computed("lastSevenDaysCount")
sevenDaysCountTitle(lastSevenDaysCount) {
return this.changeTitle(this.valueFor(8, 14), lastSevenDaysCount, "two weeks ago");
},
@computed('prev30Days', 'lastThirtyDaysCount')
thirtyDayCountTitle(prev30Days, lastThirtyDaysCount) {
return this.changeTitle(lastThirtyDaysCount, prev30Days, "in the previous 30 day period");
@computed("prev30Days", "lastThirtyDaysCount")
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
return this.changeTitle(prev30Days, lastThirtyDaysCount, "in the previous 30 day period");
},
@computed('data')
@computed("data")
sortedData(data) {
return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray();
return this.get("xAxisIsDate") ? data.toArray().reverse() : data.toArray();
},
@computed('data')
@computed("data")
xAxisIsDate() {
if (!this.data[0]) return false;
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
}
},
_computeChange(valAtT1, valAtT2) {
return ((valAtT2 - valAtT1) / valAtT1) * 100;
},
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
const change = this._computeChange(valAtT1, valAtT2);
if (change > 50) {
return higherIsBetter ? "high-trending-up" : "high-trending-down";
} else if (change > 2) {
return higherIsBetter ? "trending-up" : "trending-down";
} else if (change <= 2 && change >= -2) {
return "no-change";
} else if (change < -50) {
return higherIsBetter ? "high-trending-down" : "high-trending-up";
} else if (change < -2) {
return higherIsBetter ? "trending-down" : "trending-up";
}
},
_iconForTrend(trend, higherIsBetter) {
switch (trend) {
case "trending-up":
return higherIsBetter ? "angle-up" : "angle-down";
case "trending-down":
return higherIsBetter ? "angle-down" : "angle-up";
case "high-trending-up":
return higherIsBetter ? "angle-double-up" : "angle-double-down";
case "high-trending-down":
return higherIsBetter ? "angle-double-down" : "angle-double-up";
default:
return null;
}
}
});
Report.reopenClass({
fillMissingDates(report) {
if (_.isArray(report.data)) {
const startDateFormatted = moment.utc(report.start_date).locale('en').format('YYYY-MM-DD');
const endDateFormatted = moment.utc(report.end_date).locale('en').format('YYYY-MM-DD');
const startDateFormatted = moment.utc(report.start_date).locale("en").format("YYYY-MM-DD");
const endDateFormatted = moment.utc(report.end_date).locale("en").format("YYYY-MM-DD");
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
}
},
@ -272,7 +266,7 @@ Report.reopenClass({
// TODO: fillMissingDates if xaxis is date
const related = Report.create({ type: json.report.related_report.type });
related.setProperties(json.report.related_report);
model.set('relatedReport', related);
model.set("relatedReport", related);
}
return model;

View File

@ -1,54 +1,58 @@
import { ajax } from 'discourse/lib/ajax';
import AdminUser from 'admin/models/admin-user';
import { escapeExpression } from 'discourse/lib/utilities';
import computed from "ember-addons/ember-computed-decorators";
import { ajax } from "discourse/lib/ajax";
import AdminUser from "admin/models/admin-user";
import { escapeExpression } from "discourse/lib/utilities";
function format(label, value, escape = true) {
return value ? `<b>${I18n.t(label)}</b>: ${escape ? escapeExpression(value) : value}` : "";
};
const StaffActionLog = Discourse.Model.extend({
showFullDetails: false,
actionName: function() {
return I18n.t("admin.logs.staff_actions.actions." + this.get('action_name'));
}.property('action_name'),
formattedDetails: function() {
let formatted = "";
formatted += this.format('email', 'email');
formatted += this.format('admin.logs.ip_address', 'ip_address');
formatted += this.format('admin.logs.topic_id', 'topic_id');
formatted += this.format('admin.logs.post_id', 'post_id');
formatted += this.format('admin.logs.category_id', 'category_id');
if (!this.get('useCustomModalForDetails')) {
formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
}
if (!this.get('useModalForDetails')) {
if (this.get('details')) formatted += escapeExpression(this.get('details')) + '<br/>';
}
return formatted;
}.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
format(label, propertyName) {
if (this.get(propertyName)) {
let value = escapeExpression(this.get(propertyName));
if (propertyName === 'post_id') {
value = `<a href data-link-post-id="${value}">${value}</a>`;
}
return `<b>${I18n.t(label)}:</b> ${value}<br/>`;
} else {
return '';
}
@computed("action_name")
actionName(actionName) {
return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
},
useModalForDetails: function() {
return (this.get('details') && this.get('details').length > 100);
}.property('action_name'),
@computed("email", "ip_address", "topic_id", "post_id", "category_id", "new_value", "previous_value", "details", "useCustomModalForDetails", "useModalForDetails")
formattedDetails(email, ipAddress, topicId, postId, categoryId, newValue, previousValue, details, useCustomModalForDetails, useModalForDetails) {
const postLink = postId ? `<a href data-link-post-id="${postId}">${postId}</a>` : null;
useCustomModalForDetails: function() {
return _.contains(['change_theme', 'delete_theme'], this.get('action_name'));
}.property('action_name')
let lines = [
format("email", email),
format("admin.logs.ip_address", ipAddress),
format("admin.logs.topic_id", topicId),
format("admin.logs.post_id", postLink, false),
format("admin.logs.category_id", categoryId),
];
if (!useCustomModalForDetails) {
lines.push(format("admin.logs.staff_actions.new_value", newValue));
lines.push(format("admin.logs.staff_actions.previous_value", previousValue));
}
if (!useModalForDetails && details) {
lines = [...lines, ...escapeExpression(details).split("\n")];
}
const formatted = lines.filter(l => l.length > 0).join("<br/>");
return formatted.length > 0 ? formatted + "<br/>" : "";
},
@computed("details")
useModalForDetails(details) {
return details && details.length > 100;
},
@computed("action_name")
useCustomModalForDetails(actionName) {
return ["change_theme", "delete_theme"].includes(actionName);
}
});
StaffActionLog.reopenClass({
create: function(attrs) {
create(attrs) {
attrs = attrs || {};
if (attrs.acting_user) {
@ -60,13 +64,11 @@ StaffActionLog.reopenClass({
return this._super(attrs);
},
findAll: function(filters) {
return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then((data) => {
findAll(data) {
return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => {
return {
staff_action_logs: data.staff_action_logs.map(function(s) {
return StaffActionLog.create(s);
}),
user_history_actions: data.user_history_actions
staff_action_logs: result.staff_action_logs.map(s => StaffActionLog.create(s)),
user_history_actions: result.user_history_actions
};
});
}

View File

@ -18,7 +18,7 @@
<div>
<label for="image">{{i18n 'admin.badges.image'}}</label>
{{input type="text" name="image" value=buffered.image}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
<p class='help'>{{i18n 'admin.badges.image_help'}}</p>
</div>
<div>

View File

@ -5,18 +5,18 @@
<a href="{{report.reportUrl}}">{{report.title}}</a>
</td>
<td class="value">{{number report.todayCount}}</td>
<td class="value">{{number report.todayCount ceil=true}}</td>
<td class="value {{report.yesterdayTrend}}" title={{report.yesterdayCountTitle}}>
{{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
{{number report.yesterdayCount ceil=true}} {{d-icon report.yesterdayTrendIcon}}
</td>
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>
{{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
<td class="value {{report.sevenDaysTrend}}" title={{report.sevenDaysCountTitle}}>
{{number report.lastSevenDaysCount ceil=true}} {{d-icon report.sevenDaysTrendIcon}}
</td>
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>
{{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
<td class="value {{report.thirtyDaysTrend}}" title={{report.thirtyDaysCountTitle}}>
{{number report.lastThirtyDaysCount ceil=true}} {{d-icon report.thirtyDaysTrendIcon}}
</td>
{{#if allTime}}

View File

@ -1,40 +1,27 @@
{{#conditional-loading-section isLoading=isLoading}}
<div class="table-title">
<h3>{{title}}</h3>
{{#if help}}
<a href="{{helpPage}}">{{i18n help}}</a>
{{/if}}
</div>
{{#each reportsForPeriod as |report|}}
<div class="table-container">
<table>
<thead>
<tr>
{{#if report.labels}}
{{#each report.labels as |label|}}
<th>{{label}}</th>
{{/each}}
{{else}}
{{#each report.data as |data|}}
<th>{{data.x}}</th>
{{/each}}
{{/if}}
</tr>
</thead>
<tbody>
{{#unless hasBlock}}
{{#each report.data as |data|}}
<tr>
<td>{{number data.y}}</td>
</tr>
{{/each}}
{{else}}
{{yield (hash report=report)}}
{{/unless}}
</tbody>
</table>
{{#unless hasBlock}}
{{#each report.data as |data|}}
<a class="table-cell user-{{data.key}}" href="{{data.url}}">
<span class="label">
{{#if data.icon}}
{{d-icon data.icon}}
{{/if}}
{{data.x}}
</span>
<span class="value">
{{number data.y}}
</span>
</a>
{{/each}}
{{else}}
{{yield (hash report=report)}}
{{/unless}}
</div>
{{/each}}
{{/conditional-loading-section}}

View File

@ -14,9 +14,9 @@
<div class="trend {{report.trend}}">
<span class="trend-value" title="{{report.trendTitle}}">
{{#if report.average}}
{{number report.currentAverage}}{{#if report.percent}}%{{/if}}
{{number report.currentAverage ceil=true}}{{#if report.percent}}%{{/if}}
{{else}}
{{number report.currentTotal noTitle="true"}}{{#if report.percent}}%{{/if}}
{{number report.currentTotal ceil=true noTitle="true"}}{{#if report.percent}}%{{/if}}
{{/if}}
</span>

View File

@ -0,0 +1,36 @@
{{#conditional-loading-section isLoading=isLoading}}
<div class="table-title">
<h3>{{title}}</h3>
</div>
{{#each reportsForPeriod as |report|}}
<div class="table-container">
<table>
<thead>
<tr>
{{#if report.labels}}
{{#each report.labels as |label|}}
<th>{{label}}</th>
{{/each}}
{{else}}
{{#each report.data as |data|}}
<th>{{data.x}}</th>
{{/each}}
{{/if}}
</tr>
</thead>
<tbody>
{{#unless hasBlock}}
{{#each report.data as |data|}}
<tr>
<td>{{number data.y}}</td>
</tr>
{{/each}}
{{else}}
{{yield (hash report=report)}}
{{/unless}}
</tbody>
</table>
</div>
{{/each}}
{{/conditional-loading-section}}

View File

@ -2,6 +2,6 @@
{{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey=placeholderKey}}
{{d-button action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}}
{{#if showSuccessMessage}}
<span class="success-message">{{i18n 'admin.watched_words.form.success'}}</span>
{{#if showMessage}}
<span class="success-message">{{message}}</span>
{{/if}}

View File

@ -5,6 +5,7 @@
{{#each sortedThemes as |theme|}}
<li>
{{#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"}}

View File

@ -53,7 +53,7 @@
<div class="section-columns">
<div class="section-column">
<div class="dashboard-table">
<div class="dashboard-table activity-metrics">
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.activity_metrics")}}
<div class="table-title">
<h3>{{i18n "admin.dashboard.activity_metrics"}}</h3>
@ -79,19 +79,11 @@
</div>
{{/conditional-loading-section}}
</div>
<div class="user-metrics">
{{dashboard-inline-table dataSourceNames="users_by_type" lastRefreshedAt=lastRefreshedAt}}
{{#dashboard-inline-table dataSourceNames="users_by_type,users_by_trust_level" lastRefreshedAt=lastRefreshedAt as |context|}}
<tr>
{{#each context.report.data as |data|}}
<td>
<a href="/admin/users/list/{{data.key}}">
{{number data.y}}
</a>
</td>
{{/each}}
</tr>
{{/dashboard-inline-table}}
{{dashboard-inline-table dataSourceNames="users_by_trust_level" lastRefreshedAt=lastRefreshedAt}}
</div>
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
<div class="misc">
<div class="durability">
@ -116,7 +108,6 @@
</div>
</div>
<div class="last-dashboard-update">
<div>
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
@ -128,58 +119,57 @@
</div>
</div>
<p>
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
</p>
<p>
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
</p>
{{/conditional-loading-section}}
</div>
<div class="section-column">
<div class="top-referred-topics">
{{#dashboard-inline-table
dataSourceNames="top_referred_topics"
lastRefreshedAt=lastRefreshedAt
limit=8
as |context|}}
{{#each context.report.data as |data|}}
<tr>
<td class='left'>
<a href="{{data.topic_url}}">
{{data.topic_title}}
</a>
</td>
<td>
{{data.num_clicks}}
</td>
</tr>
{{/each}}
{{/dashboard-inline-table}}
{{#dashboard-table
dataSourceNames="top_referred_topics"
lastRefreshedAt=lastRefreshedAt
limit=8
as |context|}}
{{#each context.report.data as |data|}}
<tr>
<td class='left'>
<a href="{{data.topic_url}}">
{{data.topic_title}}
</a>
</td>
<td>
{{data.num_clicks}}
</td>
</tr>
{{/each}}
{{/dashboard-table}}
</div>
<div class="trending-search">
{{#dashboard-inline-table
limit=8
dataSourceNames="trending_search"
isEnabled=logSearchQueriesEnabled
disabledLabel="admin.dashboard.reports.trending_search.disabled"
startDate=lastWeek
endDate=endDate as |context|}}
{{#each context.report.data as |data|}}
<tr>
<td class='left'>
{{data.term}}
</td>
<td>
{{number data.unique_searches}}
</td>
<td>
{{data.ctr}}
</td>
</tr>
{{/each}}
{{/dashboard-inline-table}}
{{#dashboard-table
limit=8
dataSourceNames="trending_search"
isEnabled=logSearchQueriesEnabled
disabledLabel="admin.dashboard.reports.trending_search.disabled"
startDate=lastWeek
endDate=endDate as |context|}}
{{#each context.report.data as |data|}}
<tr>
<td class='left'>
{{data.term}}
</td>
<td>
{{number data.unique_searches}}
</td>
<td>
{{data.ctr}}
</td>
</tr>
{{/each}}
{{/dashboard-table}}
{{{i18n "admin.dashboard.reports.trending_search.more"}}}
</div>
</div>
</div>
</div>

View File

@ -4,41 +4,49 @@
<p>{{model.description}}</p>
{{/if}}
<div class="admin-reports-filter">
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
{{#if showCategoryOptions}}
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
{{/if}}
{{#if showGroupOptions}}
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
{{/if}}
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
<div class="report-container">
<div class="visualization">
{{#conditional-loading-spinner condition=refreshing}}
<div class='view-options'>
{{#if viewingTable}}
{{i18n 'admin.dashboard.reports.view_table'}}
{{else}}
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
{{/if}}
|
{{#if viewingGraph}}
{{i18n 'admin.dashboard.reports.view_graph'}}
{{else}}
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
{{/if}}
</div>
{{#if viewingGraph}}
{{admin-graph model=model}}
{{else}}
{{admin-table-report model=model}}
{{/if}}
{{#if model.relatedReport}}
{{admin-table-report model=model.relatedReport}}
{{/if}}
{{/conditional-loading-spinner}}
</div>
<div class="filters">
<span>
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate defaultDate=startDate}}
</span>
<span>
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate defaultDate=endDate}}
</span>
{{#if showCategoryOptions}}
{{combo-box filterable=true valueAttribute="value" content=categoryOptions value=categoryId}}
{{/if}}
{{#if showGroupOptions}}
{{combo-box filterable=true valueAttribute="value" content=groupOptions value=groupId}}
{{/if}}
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
</div>
</div>
<div class='view-options'>
{{#if viewingTable}}
{{i18n 'admin.dashboard.reports.view_table'}}
{{else}}
<a href {{action "viewAsTable"}}>{{i18n 'admin.dashboard.reports.view_table'}}</a>
{{/if}}
|
{{#if viewingGraph}}
{{i18n 'admin.dashboard.reports.view_graph'}}
{{else}}
<a href {{action "viewAsGraph"}}>{{i18n 'admin.dashboard.reports.view_graph'}}</a>
{{/if}}
</div>
{{#conditional-loading-spinner condition=refreshing}}
{{#if viewingGraph}}
{{admin-graph model=model}}
{{else}}
{{admin-table-report model=model}}
{{/if}}
{{#if model.relatedReport}}
{{admin-table-report model=model.relatedReport}}
{{/if}}
{{/conditional-loading-spinner}}

View File

@ -318,18 +318,18 @@
</div>
</div>
<div class="user-suspended display-row {{if model.isSuspended 'highlight-danger'}}">
<div class='field'>{{i18n 'admin.user.suspended'}}</div>
<div class='value'>
{{i18n-yes-no model.isSuspended}}
{{#if model.isSuspended}}
{{#unless model.suspendedForever}}
{{i18n "admin.user.suspended_until" until=model.suspendedTillDate}}
{{/unless}}
{{/if}}
</div>
<div class='controls'>
{{#if model.isSuspended}}
<div class="user-suspended display-row {{if model.suspended 'highlight-danger'}}">
<div class='field'>{{i18n 'admin.user.suspended'}}</div>
<div class='value'>
{{i18n-yes-no model.suspended}}
{{#if model.suspended}}
{{#unless model.suspendedForever}}
{{i18n "admin.user.suspended_until" until=model.suspendedTillDate}}
{{/unless}}
{{/if}}
</div>
<div class='controls'>
{{#if model.suspended}}
{{d-button
class="btn-danger unsuspend-user"
action=(action "unsuspend")
@ -349,7 +349,7 @@
</div>
</div>
{{#if model.isSuspended}}
{{#if model.suspended}}
<div class='display-row highlight-danger suspension-info'>
<div class='field'>{{i18n 'admin.user.suspended_by'}}</div>
<div class='value'>
@ -367,7 +367,7 @@
<div class='field'>{{i18n 'admin.user.silenced'}}</div>
<div class='value'>
{{i18n-yes-no model.silenced}}
{{#if model.isSilenced}}
{{#if model.silenced}}
{{#unless model.silencedForever}}
{{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
{{/unless}}
@ -394,7 +394,7 @@
</div>
</div>
{{#if model.isSilenced}}
{{#if model.silenced}}
<div class='display-row highlight-danger silence-info'>
<div class='field'>{{i18n 'admin.user.silenced_by'}}</div>
<div class='value'>
@ -408,6 +408,21 @@
</div>
{{/if}}
{{#if model.tl3_requirements.penalty_counts.total}}
<div class='display-row clear-penalty-history'>
<div class='field'>{{i18n 'admin.user.penalty_count'}}</div>
<div class='value'>{{model.tl3_requirements.penalty_counts.total}}</div>
{{#if currentUser.admin}}
<div class='controls'>
{{d-button label="admin.user.clear_penalty_history.title"
icon="times"
action=(action "clearPenaltyHistory")}}
{{i18n "admin.user.clear_penalty_history.description"}}
</div>
{{/if}}
</div>
{{/if}}
</section>
{{#if currentUser.admin}}
@ -468,7 +483,7 @@
{{d-button class="btn-danger" action="deleteAllPosts" icon="trash-o" label="admin.user.delete_all_posts"}}
{{/if}}
{{else}}
{{model.deleteAllPostsExplanation}}
{{deleteAllPostsExplanation}}
{{/if}}
</div>
</div>
@ -549,28 +564,26 @@
{{/if}}
{{/if}}
{{#unless model.anonymizeForbidden}}
{{#if model.can_be_anonymized}}
{{d-button label="admin.user.anonymize"
icon="exclamation-triangle"
class="btn-danger"
disabled=model.anonymizeForbidden
action="anonymize"}}
{{/unless}}
{{/if}}
{{#unless model.deleteForbidden}}
{{#if model.canBeDeleted}}
{{d-button label="admin.user.delete"
icon="exclamation-triangle"
class="btn-danger"
disabled=model.deleteForbidden
action="destroy"}}
{{/unless}}
{{/if}}
</div>
{{#if model.deleteExplanation}}
{{#if deleteExplanation}}
<div class="clearfix"></div>
<br>
<div class="pull-right">
{{d-icon "exclamation-triangle"}} {{model.deleteExplanation}}
{{d-icon "exclamation-triangle"}} {{deleteExplanation}}
</div>
{{/if}}
</section>

View File

@ -5,6 +5,7 @@
{{watched-word-form
actionKey=actionNameKey
action="recordAdded"
filteredContent=filteredContent
regularExpressions=adminWatchedWords.regularExpressions}}
{{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}}

View File

@ -1,5 +1,6 @@
<div class='admin-controls'>
<div class='controls'>
{{d-button action="toggleMenu" class="menu-toggle" icon="bars"}}
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action="clearFilter" label="admin.watched_words.clear_filter"}}
</div>

View File

@ -42,7 +42,9 @@ export default Ember.Component.extend({
this.getProperties(
'title',
'rawTitle',
'fixed'
'fixed',
'subtitle',
'rawSubtitle'
)
);
},

View File

@ -28,6 +28,7 @@ export default Ember.Component.extend({
this.appEvents.on('modal:body-shown', data => {
if (this.isDestroying || this.isDestroyed) { return; }
if (data.fixed) {
this.$().removeClass('hidden');
}
@ -37,6 +38,16 @@ export default Ember.Component.extend({
} else if (data.rawTitle) {
this.set('title', data.rawTitle);
}
if (data.subtitle) {
this.set('subtitle', I18n.t(data.subtitle));
} else if (data.rawSubtitle) {
this.set('subtitle', data.rawSubtitle);
} else {
// if no subtitle provided, makes sure the previous subtitle
// of another modal is not used
this.set('subtitle', null);
}
});
},

View File

@ -0,0 +1,18 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
tagName: 'span',
@computed("text")
translatedText(text) {
if (text) return I18n.t(text);
},
click(event) {
if (event.target.tagName.toUpperCase() === 'A') {
this.sendAction("action", this.get("actionParam"));
}
return false;
}
});

View File

@ -46,5 +46,12 @@ export default Ember.Component.extend({
}
this.set("topicTimer.updateTime", time);
}
},
@observes("selection")
_updateBasedOnLastPost() {
if (!this.get('autoClose')) {
this.set('topicTimer.based_on_last_post', false);
}
},
});

View File

@ -78,7 +78,10 @@ export default Ember.Component.extend({
const $quoteButton = this.$();
// remove the marker
markerElement.parentNode.removeChild(markerElement);
const parent = markerElement.parentNode;
parent.removeChild(markerElement);
// merge back all text nodes so they don't get messed up
parent.normalize();
// work around Safari that would sometimes lose the selection
if (isSafari) {

View File

@ -18,6 +18,7 @@ const REGEXP_TAGS_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::tag\s?$
const REGEXP_IN_MATCH = /^(in|with):(posted|watching|tracking|bookmarks|first|pinned|unpinned|wiki|unseen|image)/ig;
const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/ig;
const REGEXP_SPECIAL_IN_TITLE_MATCH = /^in:title/ig;
const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/ig;
const REGEXP_SPECIAL_IN_SEEN_MATCH = /^in:seen/ig;
@ -81,6 +82,7 @@ export default Em.Component.extend({
in: '',
special: {
in: {
title: false,
likes: false,
private: false,
seen: false
@ -111,6 +113,7 @@ export default Em.Component.extend({
this.setSearchedTermValueForTags();
this.setSearchedTermValue('searchedTerms.in', REGEXP_IN_PREFIX, REGEXP_IN_MATCH);
this.setSearchedTermSpecialInValue('searchedTerms.special.in.likes', REGEXP_SPECIAL_IN_LIKES_MATCH);
this.setSearchedTermSpecialInValue('searchedTerms.special.in.title', REGEXP_SPECIAL_IN_TITLE_MATCH);
this.setSearchedTermSpecialInValue('searchedTerms.special.in.private', REGEXP_SPECIAL_IN_PRIVATE_MATCH);
this.setSearchedTermSpecialInValue('searchedTerms.special.in.seen', REGEXP_SPECIAL_IN_SEEN_MATCH);
this.setSearchedTermValue('searchedTerms.status', REGEXP_STATUS_PREFIX);
@ -424,15 +427,14 @@ export default Em.Component.extend({
}
},
@observes('searchedTerms.special.in.likes')
updateSearchTermForSpecialInLikes() {
const match = this.filterBlocks(REGEXP_SPECIAL_IN_LIKES_MATCH);
const inFilter = this.get('searchedTerms.special.in.likes');
updateInRegex(regex, filter) {
const match = this.filterBlocks(regex);
const inFilter = this.get('searchedTerms.special.in.' + filter);
let searchTerm = this.get('searchTerm') || '';
if (inFilter) {
if (match.length === 0) {
searchTerm += ` in:likes`;
searchTerm += ` in:${filter}`;
this.set('searchTerm', searchTerm.trim());
}
} else if (match.length !== 0) {
@ -441,38 +443,24 @@ export default Em.Component.extend({
}
},
@observes('searchedTerms.special.in.likes')
updateSearchTermForSpecialInLikes() {
this.updateInRegex(REGEXP_SPECIAL_IN_LIKES_MATCH, 'likes');
},
@observes('searchedTerms.special.in.private')
updateSearchTermForSpecialInPrivate() {
const match = this.filterBlocks(REGEXP_SPECIAL_IN_PRIVATE_MATCH);
const inFilter = this.get('searchedTerms.special.in.private');
let searchTerm = this.get('searchTerm') || '';
if (inFilter) {
if (match.length === 0) {
searchTerm += ` in:private`;
this.set('searchTerm', searchTerm.trim());
}
} else if (match.length !== 0) {
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm.trim());
}
this.updateInRegex(REGEXP_SPECIAL_IN_PRIVATE_MATCH, 'private');
},
@observes('searchedTerms.special.in.seen')
updateSearchTermForSpecialInSeen() {
const match = this.filterBlocks(REGEXP_SPECIAL_IN_SEEN_MATCH);
const inFilter = this.get('searchedTerms.special.in.seen');
let searchTerm = this.get('searchTerm') || '';
this.updateInRegex(REGEXP_SPECIAL_IN_SEEN_MATCH, 'seen');
},
if (inFilter) {
if (match.length === 0) {
searchTerm += ` in:seen`;
this.set('searchTerm', searchTerm.trim());
}
} else if (match.length !== 0) {
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm.trim());
}
@observes('searchedTerms.special.in.title')
updateSearchTermForSpecialInTitle() {
this.updateInRegex(REGEXP_SPECIAL_IN_TITLE_MATCH, 'title');
},
@observes('searchedTerms.status')

View File

@ -16,7 +16,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
_topic: null,
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
_notificationsChanged() {
notificationsChanged() {
this.queueRerender();
},

View File

@ -18,7 +18,6 @@ export default Ember.Component.extend(bufferedRender({
if (!this.get('executeAt')) return;
let statusUpdateAt = moment(this.get('executeAt'));
if (statusUpdateAt < new Date()) return;
let duration = moment.duration(statusUpdateAt - moment());
let minutesLeft = duration.asMinutes();

View File

@ -18,7 +18,6 @@ export default Ember.Component.extend(CardContentsBase, CanCheckEmails, CleansUp
showFilter: Ember.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'),
showName: propertyNotEqual('user.name', 'user.username'),
hasUserFilters: Ember.computed.gt('postStream.userFilters.length', 0),
isSuspended: Ember.computed.notEmpty('user.suspend_reason'),
showMoreBadges: Ember.computed.gt('moreBadgesCount', 0),
showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
linkWebsite: Ember.computed.not('user.isBasic'),

View File

@ -11,5 +11,5 @@ export default Ember.Component.extend({
],
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
actionDescription: actionDescription("item.action_code", "item.created_at", "item.action_code_who"),
});

View File

@ -146,6 +146,11 @@ export default Ember.Controller.extend({
}
},
@computed('q')
isPrivateMessage(q) {
return q && this.currentUser && (q.indexOf("in:private") > -1 || q.indexOf(`private_messages:${this.currentUser.get('username_lower')}`) > -1);
},
@observes('loading')
_showFooter() {
this.set("application.showFooter", !this.get("loading"));

View File

@ -4,6 +4,7 @@ import { default as computed } from "ember-addons/ember-computed-decorators";
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
import { setting } from 'discourse/lib/computed';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import showModal from 'discourse/lib/show-modal';
export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, {
@ -41,11 +42,6 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
},
@computed
showTwoFactorModalText() {
return I18n.t('user.second_factor.title').toLowerCase();
},
actions: {
save() {
this.set('saved', false);
@ -105,6 +101,10 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
}
];
bootbox.dialog(message, buttons, {"classes": "delete-account"});
},
showTwoFactorModal() {
showModal('second-factor-intro');
}
}
});

View File

@ -14,6 +14,20 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
import { userPath } from 'discourse/lib/url';
let customPostMessageCallbacks = {};
export function resetCustomPostMessageCallbacks() {
customPostMessageCallbacks = {};
}
export function registerCustomPostMessageCallback(type, callback) {
if (customPostMessageCallbacks[type]) {
throw `Error ${type} is an already registered post message!`;
}
customPostMessageCallbacks[type] = callback;
}
export default Ember.Controller.extend(BufferedContent, {
composer: Ember.inject.controller(),
application: Ember.inject.controller(),
@ -935,7 +949,12 @@ export default Ember.Controller.extend(BufferedContent, {
break;
}
default: {
Em.Logger.warn("unknown topic bus message type", data);
let callback = customPostMessageCallbacks[data.type];
if (callback) {
callback(this, data);
} else {
Em.Logger.warn("unknown topic bus message type", data);
}
}
}

View File

@ -39,9 +39,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
showStaffCounters: Ember.computed.or('hasGivenFlags', 'hasFlaggedPosts', 'hasDeletedPosts', 'hasBeenSuspended', 'hasReceivedWarnings'),
@computed('model.isSuspended', 'currentUser.staff')
isNotSuspendedOrIsStaff(isSuspended, isStaff) {
return !isSuspended || isStaff;
@computed('model.suspended', 'currentUser.staff')
isNotSuspendedOrIsStaff(suspended, isStaff) {
return !suspended || isStaff;
},
linkWebsite: Em.computed.not('model.isBasic'),

View File

@ -8,6 +8,10 @@ registerUnbound('raw-date', dt => longDate(new Date(dt)));
registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
registerUnbound('number', (orig, params) => {
if (params.ceil) {
orig = Math.ceil(orig);
}
orig = parseInt(orig, 10);
if (isNaN(orig)) { orig = 0; }

View File

@ -6,9 +6,10 @@ export default {
(location.hostname === "localhost");
const isSupported= isSecured && ('serviceWorker' in navigator);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSupported) {
if (Discourse.ServiceWorkerURL) {
if (Discourse.ServiceWorkerURL && !isSafari) {
navigator.serviceWorker
.register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`)
.catch(error => {

View File

@ -35,7 +35,6 @@ export default {
bus.subscribe(`/notification/${user.get('id')}`, data => {
const store = container.lookup('service:store');
const oldUnread = user.get('unread_notifications');
const oldPM = user.get('unread_private_messages');
@ -66,7 +65,7 @@ export default {
oldNotifications.insertAt(insertPosition, Em.Object.create(lastNotification));
}
for (let idx=0; idx < data.recent.length; idx++) {
for (let idx = 0; idx < data.recent.length; idx++) {
let old;
while(old = oldNotifications[idx]) {
const info = data.recent[idx];
@ -96,21 +95,16 @@ export default {
});
bus.subscribe("/client_settings", data => Ember.set(siteSettings, data.name, data.value));
bus.subscribe("/refresh_client", data => {
Discourse.set("assetVersion", data);
});
bus.subscribe("/refresh_client", data => Discourse.set("assetVersion", data));
if (!Ember.testing) {
bus.subscribe(alertChannel(user), data => onNotification(data, user));
initDesktopNotifications(bus, appEvents);
if(isPushNotificationsEnabled(user, site.mobileView)) {
if (isPushNotificationsEnabled(user, site.mobileView)) {
disableDesktopNotifications();
registerPushNotifications(Discourse.User.current(), site.mobileView, router, appEvents);
}
else {
} else {
unsubscribePushNotifications(user);
}
}

View File

@ -301,9 +301,13 @@ export function relativeAge(date, options) {
return "UNKNOWN FORMAT";
}
export function number(val) {
export function number(val, options = {}) {
let formattedNumber;
if (options.ceil) {
val = Math.ceil(val);
}
val = parseInt(val, 10);
if (isNaN(val)) val = 0;

View File

@ -25,9 +25,10 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
import { addGTMPageChangedCallback } from 'discourse/lib/page-tracker';
import { registerCustomAvatarHelper } from 'discourse/helpers/user-avatar';
import { disableNameSuppression } from 'discourse/widgets/poster-name';
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from 'discourse/controllers/topic';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = '0.8.21';
const PLUGIN_API_VERSION = '0.8.22';
class PluginApi {
constructor(version, container) {
@ -426,6 +427,24 @@ class PluginApi {
disableNameSuppression();
}
/**
* Registers a callback that will be invoked when the server calls
* Post#publish_change_to_clients! please ensure your type does not
* match acted,revised,rebaked,recovered, created,move_to_inbox or archived
*
* callback will be called with topicController and Message
*
* Example:
*
* api.registerCustomPostMessageCallback("applied_color", (topicController, message) => {
* let stream = topicController.get("model.postStream");
* // etc
* });
*/
registerCustomPostMessageCallback(type, callback) {
registerCustomPostMessageCallback1(type, callback);
}
/**
* Changes a setting associated with a widget. For example, if
* you wanted small avatars in the post stream:

View File

@ -169,8 +169,6 @@ const User = RestModel.extend({
isElder: Em.computed.equal('trust_level', 4),
canManageTopic: Em.computed.or('staff', 'isElder'),
isSuspended: Em.computed.equal('suspended', true),
@computed("previous_visit_at")
previousVisitAt(previous_visit_at) {
return new Date(previous_visit_at);

View File

@ -6,6 +6,7 @@ export default Discourse.Route.extend({
beforeModel(transition) {
const self = this;
const params = transition.queryParams;
const groupName = params.groupname || params.group_name;
if (self.currentUser) {
self.replaceWith("discovery.latest").then(e => {
@ -20,13 +21,13 @@ export default Discourse.Route.extend({
}).catch(function() {
bootbox.alert(I18n.t("generic_error"));
});
} else if (params.groupname) {
} else if (groupName) {
// send a message to a group
Group.messageable(params.groupname).then(result => {
Group.messageable(groupName).then(result => {
if (result.messageable) {
Ember.run.next(() => e.send("createNewMessageViaParams", params.groupname, params.title, params.body));
Ember.run.next(() => e.send("createNewMessageViaParams", groupName, params.title, params.body));
} else {
bootbox.alert(I18n.t("composer.cant_send_pm", { username: params.groupname }));
bootbox.alert(I18n.t("composer.cant_send_pm", { username: groupName }));
}
}).catch(function() {
bootbox.alert(I18n.t("generic_error"));

View File

@ -15,10 +15,6 @@ export default RestrictedUserRoute.extend({
},
actions: {
showTwoFactorModal() {
showModal('second-factor-intro');
},
showAvatarSelector() {
showModal('avatar-selector');

View File

@ -7,7 +7,7 @@
category=secondCategory
parentCategory=firstCategory
categories=childCategories
subCategory="true"
subCategory=true
noSubcategories=noSubcategories}}
{{/if}}

View File

@ -6,7 +6,13 @@
<a class="close" {{action closeModal}}>{{d-icon "times"}}</a>
</div>
<h3>{{title}}</h3>
<div class="title">
<h3>{{title}}</h3>
{{#if subtitle}}
<p>{{subtitle}}</p>
{{/if}}
</div>
</div>
<div id='modal-alert'></div>

View File

@ -0,0 +1 @@
{{{translatedText}}}

View File

@ -69,7 +69,7 @@
<div class="controls">
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
{{#unless isDefaultSortOrder}}
{{combo-box valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
{{combo-box castBoolean=true valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
{{/unless}}
</div>
</section>

View File

@ -9,7 +9,7 @@
input=topicTimer.updateTime
statusType=selection
includeWeekend=true
basedOnLastPost=false}}
basedOnLastPost=topicTimer.based_on_last_post}}
{{else if publishToCategory}}
<div class="control-group">
<label>{{i18n 'topic.topic_status_update.publish_to'}}</label>
@ -23,7 +23,7 @@
statusType=selection
includeWeekend=true
categoryId=topicTimer.category_id
basedOnLastPost=false}}
basedOnLastPost=topicTimer.based_on_last_post}}
{{else if autoClose}}
{{future-date-input
input=topicTimer.updateTime

View File

@ -60,6 +60,7 @@
<div class="controls">
{{#if currentUser}}
<section class='field'>
<label>{{input type="checkbox" class="in-title" checked=searchedTerms.special.in.title}} {{i18n "search.advanced.filters.title"}}</label>
<label>{{input type="checkbox" class="in-likes" checked=searchedTerms.special.in.likes}} {{i18n "search.advanced.filters.likes"}}</label>
<label>{{input type="checkbox" class="in-private" checked=searchedTerms.special.in.private}} {{i18n "search.advanced.filters.private"}}</label>
<label>{{input type="checkbox" class="in-seen" checked=searchedTerms.special.in.seen}} {{i18n "search.advanced.filters.seen"}}</label>

View File

@ -84,7 +84,7 @@
args=(hash user=user close=(action "close"))
tagName=""}}
{{#if isSuspended}}
{{#if user.suspend_reason}}
<div class='suspended'>
{{d-icon "ban"}}
<b>{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}</b><br>

View File

@ -85,7 +85,7 @@
{{/if}}
{{category-link result.topic.category hideParent=true}}
{{#each result.topic.tags as |tag|}}
{{discourse-tag tag}}
{{discourse-tag tag isPrivateMessage=isPrivateMessage}}
{{/each}}
{{plugin-outlet name="full-page-search-category" args=(hash result=result)}}
</div>

View File

@ -1,11 +1,10 @@
{{#load-more selector=".user-stream-item" action="loadMore"}}
<div class="user-stream">
{{#each model as |post|}}
{{group-post post=post class="user-stream-item item"}}
{{else}}
<div>{{i18n emptyText}}</div>
{{/each}}
{{#each model as |post|}}
{{group-post post=post class="user-stream-item item"}}
{{else}}
<div>{{i18n emptyText}}</div>
{{/each}}
</div>
{{conditional-loading-spinner condition=loading}}
{{/load-more}}

View File

@ -1,9 +1,7 @@
<div class="group-members-actions">
{{#if hasMembers}}
{{text-field value=filterInput
placeholderKey=filterPlaceholder
class="group-username-filter no-blur"}}
{{/if}}
<div class="group-members-manage">
{{#if canManageGroup}}

View File

@ -9,7 +9,7 @@
<div class="groups-header-filters">
{{text-field value=filterInput
placeholderKey="groups.index.all_groups"
placeholderKey="groups.index.all"
class="groups-header-filters-name no-blur"}}
{{combo-box value=type

View File

@ -14,7 +14,7 @@
<table>
<tr>
<td>
<label for='login-account-name'>{{i18n 'login.username'}}&nbsp;</label>
<label for='login-account-name'>{{i18n 'login.username'}}</label>
</td>
<td>
{{text-field value=loginName type="email" placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off"}}
@ -32,10 +32,10 @@
{{/if}}
<tr>
<td>
<label for='login-account-password'>{{i18n 'login.password'}}&nbsp;</label>
<label for='login-account-password'>{{i18n 'login.password'}}</label>
</td>
<td>
{{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}} &nbsp;
{{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}}
</td>
</tr>
<tr>
@ -51,8 +51,7 @@
{{/second-factor-form}}
</form>
{{/if}}
{{authMessage}}
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
{{/d-modal-body}}
<div class="modal-footer">
@ -74,4 +73,6 @@
{{/if}}
{{/if}}
</div>
<div class="auth-message">{{authMessage}}</div>
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
{{/login-modal}}

View File

@ -1,6 +1,7 @@
{{#d-modal
modalClass=modalClass
title=title
subtitle=subtitle
class="hidden"
errors=errors
closeModal=(route-action "closeModal")}}

View File

@ -1,13 +1,13 @@
{{#create-account email=accountEmail disabled=submitDisabled action="createAccount"}}
{{#unless complete}}
{{#d-modal-body title="create_account.title"}}
{{#unless hasAuthOptions}}
{{login-buttons externalLogin="externalLogin"}}
{{/unless}}
{{#if showCreateForm}}
<div>
<div class="login-form">
<form>
<table>
@ -15,12 +15,13 @@
<td class="label"><label for='new-account-email'>{{i18n 'user.email.title'}}</label></td>
<td>
{{input type="email" value=accountEmail id="new-account-email" disabled=emailValidated name="email" autofocus="autofocus"}}
&nbsp;{{input-tip validation=emailValidation}}
</td>
</tr>
<tr class="instructions create-account-email">
<td></td>
<td><label>{{i18n 'user.email.instructions'}}</label></td>
{{input-tip validation=emailValidation}}
<td><label>{{i18n 'user.email.instructions'}}</label></td>
</tr>
{{#if usernameRequired}}
@ -28,11 +29,11 @@
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></td>
<td>
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}}
&nbsp;{{input-tip validation=usernameValidation id="username-validation"}}
</td>
</tr>
<tr class="instructions">
<td></td>
{{input-tip validation=usernameValidation id="username-validation"}}
<td><label>{{i18n 'user.username.instructions'}}</label></td>
</tr>
{{/if}}
@ -43,11 +44,12 @@
<label for='new-account-name'>{{i18n 'user.name.title'}}</label>
</td>
<td>
{{text-field value=accountName id="new-account-name"}}&nbsp;{{input-tip validation=nameValidation}}
{{text-field value=accountName id="new-account-name"}}
</td>
</tr>
<tr class="instructions">
<td></td>
{{input-tip validation=nameValidation}}
<td><label>{{nameInstructions}}</label></td>
</tr>
{{/if}}
@ -63,14 +65,14 @@
<td class="label"><label for='new-account-password'>{{i18n 'user.password.title'}}</label></td>
<td>
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
&nbsp;{{input-tip validation=passwordValidation}}
</td>
</tr>
<tr class="instructions">
<td></td>
{{input-tip validation=passwordValidation}}
<td>
<label>{{passwordInstructions}}</label>
<div class="caps-lock-warning {{unless capsLockOn 'invisible'}}">
<div class="caps-lock-warning {{unless capsLockOn 'hidden'}}">
{{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div>
</td>
</tr>
@ -97,6 +99,7 @@
</form>
</div>
{{/if}}
{{/d-modal-body}}
{{#if showCreateForm}}

View File

@ -1,12 +1,5 @@
{{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}}
{{#d-modal-body title="login.title" class="login-modal"}}
{{#if showLoginButtons}}
{{login-buttons
canLoginLocalWithEmail=canLoginLocalWithEmail
processingEmailLink=processingEmailLink
emailLogin='emailLogin'
externalLogin='externalLogin'}}
{{/if}}
{{#if canLoginLocal}}
<form id='login-form' method='post'>
@ -30,7 +23,7 @@
</tr>
<tr>
<td></td>
<td><div class="caps-lock-warning {{unless capsLockOn 'invisible'}}">{{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div></td>
<td><div class="caps-lock-warning {{unless capsLockOn 'hidden'}}">{{d-icon "exclamation-triangle"}} {{i18n 'login.caps_lock_warning'}}</div></td>
<td></td>
</tr>
</table>
@ -40,8 +33,14 @@
{{/second-factor-form}}
</form>
{{/if}}
{{authMessage}}
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
{{#if showLoginButtons}}
{{login-buttons
canLoginLocalWithEmail=canLoginLocalWithEmail
processingEmailLink=processingEmailLink
emailLogin='emailLogin'
externalLogin='externalLogin'}}
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
@ -65,4 +64,7 @@
{{conditional-loading-spinner condition=showSpinner size="small"}}
</div>
<div class="auth-message">{{authMessage}}</div>
<div id='login-alert' class={{alertClass}}>{{alert}}</div>
{{/login-modal}}

View File

@ -71,9 +71,7 @@
{{#if model.second_factor_enabled}}
{{i18n 'user.second_factor.disable'}}
{{else}}
{{i18n 'enable'}}
<a href {{action "showTwoFactorModal"}}>{{showTwoFactorModalText}}</a>
{{i18n 'user.second_factor.enable'}}
{{discourse-linked-text action="showTwoFactorModal" text="user.second_factor.enable"}}
{{/if}}
{{#if isCurrentUser}}

View File

@ -1,3 +1,10 @@
{{#unless siteSettings.disable_mailing_list_mode}}
<div class="warning-wrap">
{{#if model.user_option.mailing_list_mode}}
<div class="warning">{{i18n 'user.mailing_list_mode.warning'}}</div>
{{/if}}
</div>
{{/unless}}
<div class="control-group pref-email-settings">
<label class="control-label">{{i18n 'user.email_settings'}}</label>
<div class='controls controls-dropdown'>

View File

@ -16,7 +16,7 @@
<div class="instructions">{{i18n 'user.watched_tags_instructions'}}</div>
<div class="controls tag-controls">
<label>{{d-icon "d-regular" class="icon tracking"}} {{i18n 'user.tracked_tags'}}</label>
<label>{{d-icon "d-tracking" class="icon tracking"}} {{i18n 'user.tracked_tags'}}</label>
{{tag-chooser
tags=model.tracked_tags
blacklist=selectedTags

View File

@ -107,7 +107,7 @@
</h3>
<div class='bio'>
{{#if model.isSuspended}}
{{#if model.suspended}}
<div class='suspended'>
{{d-icon "ban"}}
<b>

View File

@ -69,7 +69,7 @@ registerButton('like-count', attrs => {
const title = attrs.liked
? count === 1 ? 'post.has_likes_title_only_you' : 'post.has_likes_title_you'
: 'post.has_likes_title';
const icon = attrs.yours ? 'heart' : '';
const icon = attrs.yours ? 'd-liked' : '';
const additionalClass = attrs.yours ? 'my-likes' : 'regular-likes';
return { action: 'toggleWhoLiked',

View File

@ -105,7 +105,7 @@ export default createWidget('search-menu', {
if (contextEnabled && ctx) {
if (this.currentUser &&
ctx.id.toString().toLowerCase() === this.currentUser.username_lower &&
ctx.id.toString().toLowerCase() === this.currentUser.get('username_lower') &&
type === "private_messages") {
query += ' in:private';
} else {

View File

@ -4,6 +4,8 @@ import { h } from 'virtual-dom';
import { relativeAge } from 'discourse/lib/formatter';
import { iconNode } from 'discourse-common/lib/icon-library';
import RawHtml from 'discourse/widgets/raw-html';
import renderTags from 'discourse/lib/render-tags';
import renderTopicFeaturedLink from 'discourse/lib/render-topic-featured-link';
const SCROLLER_HEIGHT = 50;
const LAST_READ_HEIGHT = 20;
@ -382,6 +384,7 @@ export default createWidget('topic-timeline', {
const createdAt = new Date(topic.created_at);
const stream = attrs.topic.get('postStream.stream');
const { currentUser } = this;
const { tagging_enabled, topic_featured_link_enabled } = this.siteSettings;
attrs["currentUser"] = currentUser;
@ -399,6 +402,34 @@ export default createWidget('topic-timeline', {
action: 'jumpTop'
}))];
// duplicate of the {{topic-category}} component
let category = [];
if (!topic.get("isPrivateMessage")) {
if (topic.category.parentCategory) {
category.push(this.attach("category-link", { category: topic.category.parentCategory }));
}
category.push(this.attach("category-link", { category: topic.category }));
}
const showTags = tagging_enabled && topic.tags && topic.tags.length > 0;
if (showTags || topic_featured_link_enabled) {
let extras = [];
if (showTags) {
const tagsHtml = new RawHtml({ html: renderTags(topic, { mode: "list" }) });
extras.push(h("div.list-tags", tagsHtml));
}
if (topic_featured_link_enabled) {
extras.push(new RawHtml({ html: renderTopicFeaturedLink(topic) }));
}
category.push(h("div.topic-header-extra", extras));
}
if (category.length > 0) {
elems.push(h("div.topic-category", category));
}
if (this.state.excerpt) {
elems.push(new RawHtml({
html: `<div class='post-excerpt'>${this.state.excerpt}</div>`

View File

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

View File

@ -11,7 +11,7 @@ function trailingSpaceOnly(src, start, max) {
return true;
}
const ATTR_REGEX = /^\s*=(.+)$|((([a-z0-9]*)\s*)=)(["“”'].*["“”']|\S+)/ig;
const ATTR_REGEX = /^\s*=(.+)$|((([a-z0-9]*)\s*)=)(["“”'].*?["“”']|\S+)/ig;
// parse a tag [test a=1 b=2] to a data structure
// {tag: "test", attrs={a: "1", b: "2"}

View File

@ -20,6 +20,7 @@ export default ComboBoxComponent.extend({
fullWidthOnMobile: true,
caretDownIcon: "caret-right",
caretUpIcon: "caret-down",
subCategory: false,
init() {
this._super();
@ -50,13 +51,15 @@ export default ComboBoxComponent.extend({
collectionHeader(allCategoriesUrl, allCategoriesLabel, noCategoriesUrl, noCategoriesLabel) {
let shortcuts = "";
shortcuts += `
<a href="${allCategoriesUrl}" class="category-filter">
${allCategoriesLabel}
</a>
`;
if (this.get("hasSelection") || (this.get("noSubcategories") && this.get("subCategory"))) {
shortcuts += `
<a href="${allCategoriesUrl}" class="category-filter">
${allCategoriesLabel}
</a>
`;
}
if (this.get("subCategory")) {
if (this.get("subCategory") && (this.get("hasSelection") || !this.get("noSubcategories"))) {
shortcuts += `
<a href="${noCategoriesUrl}" class="category-filter">
${noCategoriesLabel}

View File

@ -28,6 +28,7 @@ export default DropdownSelectBoxComponent.extend({
allowInitialValueMutation: false,
allowAutoSelectFirst: false,
showFullTitle: false,
isHidden: Ember.computed.empty("content"),
didReceiveAttrs() {
this._super();
@ -68,7 +69,7 @@ export default DropdownSelectBoxComponent.extend({
content(options, canWhisper, action) {
let items = [];
if (action !== CREATE_TOPIC && action !== CREATE_SHARED_DRAFT) {
if (action !== CREATE_TOPIC && action !== CREATE_SHARED_DRAFT && _topicSnapshot) {
items.push({
name: I18n.t("composer.composer_actions.reply_as_new_topic.label"),
description: I18n.t("composer.composer_actions.reply_as_new_topic.desc"),

View File

@ -155,7 +155,7 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
},
mutateValue(value) {
if (this.get("isCustom") || this.get("isBasedOnLastPost")) {
if (value === 'pick_date_and_time' || this.get("isBasedOnLastPost")) {
this.set("value", value);
} else {
let input = null;

View File

@ -29,11 +29,15 @@ export default ComboBoxComponent.extend({
@computed
collectionHeader() {
return `
<a href="${Discourse.getURL("/groups")}" class="group-dropdown-filter">
${I18n.t("groups.index.all").toLowerCase()}
</a>
`.htmlSafe();
if (this.siteSettings.enable_group_directory ||
(this.currentUser && this.currentUser.get('staff'))) {
return `
<a href="${Discourse.getURL("/groups")}" class="group-dropdown-filter">
${I18n.t("groups.index.all").toLowerCase()}
</a>
`.htmlSafe();
}
},
actions: {

View File

@ -79,7 +79,7 @@ export default SelectKitComponent.extend({
shouldDisplayFilter() { return true; },
_beforeWillComputeValues(values) {
return values.map(v => this._castInteger(v === "" ? null : v));
return values.map(v => this._cast(v === "" ? null : v));
},
willComputeValues(values) { return values; },
computeValues(values) { return values; },

View File

@ -62,6 +62,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
horizontalOffset: 0,
fullWidthOnMobile: false,
castInteger: false,
castBoolean: false,
allowAny: false,
allowInitialValueMutation: false,
content: null,
@ -169,7 +170,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
}
let computedContentItem = {
value: this._castInteger(this.valueForContentItem(contentItem)),
value: this._cast(this.valueForContentItem(contentItem)),
name: name || this._nameForContent(contentItem),
locked: false,
created: options.created || false,
@ -333,7 +334,10 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
},
stopLoading() {
this.focus();
if (this.site && !this.site.isMobileDevice) {
this.focusFilterOrHeader();
}
this.set("isLoading", false);
this._boundaryActionHandler("onStopLoading");
},

View File

@ -9,7 +9,7 @@ export default Ember.Component.extend({
"tabindex",
"ariaLabel:aria-label",
"ariaHasPopup:aria-haspopup",
"title",
"sanitizedTitle:title",
"value:data-value",
"name:data-name",
],
@ -18,7 +18,7 @@ export default Ember.Component.extend({
ariaHasPopup: true,
ariaLabel: Ember.computed.or("computedContent.ariaLabel", "title"),
ariaLabel: Ember.computed.or("computedContent.ariaLabel", "sanitizedTitle"),
@computed("computedContent.title", "name")
title(computedContentTitle, name) {
@ -28,6 +28,13 @@ export default Ember.Component.extend({
return null;
},
// this might need a more advanced solution
// but atm it's the only case we have to handle
@computed("title")
sanitizedTitle(title) {
return String(title).replace("&hellip;", "");
},
label: Ember.computed.or("computedContent.label", "title", "name"),
name: Ember.computed.alias("computedContent.name"),

View File

@ -63,7 +63,7 @@ export default SelectKitComponent.extend({
switch (typeof value) {
case "string":
case "number":
return this._castInteger(value === "" ? null : value);
return this._cast(value === "" ? null : value);
default:
return value;
}

View File

@ -77,14 +77,25 @@ export default ComboBoxComponent.extend({
@computed("allTagsUrl", "allTagsLabel", "noTagsUrl", "noTagsLabel")
collectionHeader(allTagsUrl, allTagsLabel, noTagsUrl, noTagsLabel) {
return `
<a href="${allTagsUrl}" class="tag-filter">
${allTagsLabel}
</a>
<a href="${noTagsUrl}" class="tag-filter">
${noTagsLabel}
</a>
`;
let content = "";
if (this.get("tagId") !== "none") {
content += `
<a href="${noTagsUrl}" class="tag-filter">
${noTagsLabel}
</a>
`;
}
if (this.get("hasSelection") || this.get("tagId") === "none") {
content += `
<a href="${allTagsUrl}" class="tag-filter">
${allTagsLabel}
</a>
`;
}
return content;
},
@computed("tag")

View File

@ -28,7 +28,6 @@ export default Ember.Mixin.create({
})
.finally(() => {
self.stopLoading();
self.focusFilterOrHeader();
});
},

View File

@ -27,6 +27,19 @@ export default Ember.Mixin.create({
return !isNaN(parseFloat(input)) && isFinite(input);
},
_cast(value) {
if (value === this.noneValue) return value;
return this._castInteger(this._castBoolean(value));
},
_castBoolean(value) {
if (this.get("castBoolean") && Ember.isPresent(value) && typeof(value) === "string") {
return value === "true";
}
return value;
},
_castInteger(value) {
if (this.get("castInteger") && Ember.isPresent(value) && this._isNumeric(value)) {
return parseInt(value, 10);

View File

@ -37,6 +37,8 @@ self.addEventListener('install', function(event) {
return caches.open(CURRENT_CACHES.offline).then(function(cache) {
return cache.put(OFFLINE_URL, response);
});
}).then(function(cache) {
self.skipWaiting();
})
);
});
@ -60,11 +62,17 @@ self.addEventListener('activate', function(event) {
}
})
);
}).then(function() {
self.clients.claim()
})
);
});
self.addEventListener('fetch', function(event) {
// Bypass service workers if this is a url with a token param
if(/\?.*token/i.test(event.request.url)) {
return;
}
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
// request.mode of 'navigate' is unfortunately not supported in Chrome

View File

@ -6,6 +6,7 @@
@import "common/admin/customize";
@import "common/admin/flagging";
@import "common/admin/dashboard_next";
@import "common/admin/admin_reports";
@import "common/admin/moderation_history";
@import "common/admin/suspend";
@ -880,7 +881,6 @@ section.details {
.upgrade-header {
flex: 1 1 100%;
margin: .25em 0 1em 0;
@media screen and (max-width: 650px) {
margin: 0;
}
@ -1969,6 +1969,12 @@ table#user-badges {
margin-right: 20px;
}
.admin-reports, .dashboard-next {
&.admin-contents {
margin: 0;
}
}
.cbox0 { background: blend-primary-secondary(0%); }
.cbox10 { background: blend-primary-secondary(10%); }
.cbox20 { background: blend-primary-secondary(20%); }

View File

@ -0,0 +1,53 @@
.admin-reports {
h3 {
border-bottom: 1px solid $primary-low;
margin-bottom: .5em;
padding-bottom: .5em;
}
.report-container {
display: flex;
.loading-container {
width: 100%;
}
.visualization {
display: flex;
flex: 4;
}
.filters {
display: flex;
flex: 1;
align-items: center;
flex-direction: column;
margin-left: 2em;
.date-picker {
margin: 0;
width: 195px;
}
.combo-box, .date-picker-wrapper, .btn {
width: 100%;
margin-bottom: 1em;
}
}
@include small-width {
flex-direction: column;
min-width: 100%;
.visualization {
order: 2;
}
.filters {
order: 1;
margin: 0;
align-items: flex-start;
}
}
}
}

View File

@ -1,8 +1,4 @@
.dashboard-next {
&.admin-contents {
margin: 0;
}
.section-top {
margin-bottom: 1em;
}
@ -17,6 +13,15 @@
.section-column {
min-width: calc(50% - .5em);
max-width: 100%;
&:last-child, {
margin-left: 1em;
}
&:first-child {
margin-right: 1em;
}
@include small-width {
min-width: 100%;
@ -31,16 +36,9 @@
}
}
.section-column:last-child, {
margin-left: 1em;
}
.section-column:first-child {
margin-right: 1em;
}
@include small-width {
.section-column:last-child, .section-column:first-child {
.section-column:last-child,
.section-column:first-child {
margin: 0;
}
}
@ -53,6 +51,9 @@
}
display: flex;
@media screen and (max-width: 400px) {
flex-wrap: wrap;
}
align-items: center;
justify-content: space-between;
border-bottom: 1px solid $primary-low;
@ -61,7 +62,7 @@
}
.section-body {
padding: 1em 0;
padding: 1em 0 0;
}
}
@ -103,16 +104,17 @@
.durability, .last-dashboard-update {
flex: 1 1 50%;
box-sizing: border-box;
margin: 20px 0;
padding: 0 20px;
margin: 1em 0;
padding: 0 1em;
}
.durability {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.backups, .uploads {
flex: 1 1 100%;
flex: 1 1 100%;
}
.uploads p:last-of-type {
@ -142,7 +144,7 @@
border-left: 1px solid $primary-low;
text-align: center;
display: flex;
justify-content: center;
justify-content: center;
div {
align-self: center;
h4 {
@ -152,7 +154,17 @@
}
}
.top-referred-topics, .trending-search {
th:first-of-type {
text-align: left;
}
}
.top-referred-topics {
.dashboard-table table {
table-layout: auto;
}
}
.community-health {
.period-chooser .period-chooser-header {
@ -167,7 +179,6 @@
}
}
.dashboard-mini-chart {
.status {
display: flex;
@ -186,6 +197,10 @@
cursor: pointer;
margin-left: .25em;
color: $primary-low-mid;
&:hover {
color: $primary-medium;
}
}
}
@ -200,6 +215,10 @@
color: $success;
}
&.no-change {
color: $primary-medium;
}
.trend-value {
font-size: $font-up-1;
}
@ -217,6 +236,7 @@
justify-content: space-between;
flex: 1;
width: 100%;
min-width: 0;
}
@include small-width {
@ -234,7 +254,7 @@
width: 100%;
}
.d-icon-question-circle {
.tooltip {
cursor: pointer;
}
@ -250,30 +270,6 @@
}
}
}
&.high-trending-up, &.trending-up {
.chart-trend, .data-point {
color: $success;
}
}
&.high-trending-down, &.trending-down {
.chart-trend, .data-point {
color: $danger;
}
}
}
.top-referred-topics, .trending-search {
th:first-of-type {
text-align: left;
}
}
.top-referred-topics {
.dashboard-table table {
table-layout: auto;
}
}
.dashboard-table {
@ -335,6 +331,7 @@
text-align: center;
padding: 8px;
}
td.left {
text-align: left;
}
@ -345,9 +342,11 @@
td.value {
text-align: right;
transform: translateX(-40%);
padding: 8px 21px 8px 8px; // accounting for negative right caret margin
&:nth-of-type(2) {
padding: 8px 12px 8px;
}
i {
display: none;
margin-right: -12px; // align on caret
@media screen and (max-width: 650px) {
margin-right: -9px;
@ -355,25 +354,158 @@
}
&.high-trending-up, &.trending-up {
i.up {
color: $success;
display: inline;
}
i { color: $success; }
}
&.high-trending-down, &.trending-down {
i.down {
color: $danger;
display: inline;
}
}
&.no-change {
i.down {
display: inline;
visibility: hidden;
}
i { color: $danger; }
}
}
}
}
}
}
.user-metrics {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-left: -5%;
margin: 2em 0 .75em -5%; // Negative margin allows for a margin when in 2-columns,
.dashboard-inline-table { // and "hides" margin when the item spans 100% width
flex: 1 0 auto;
max-width: 95%;
}
.table-cell {
display: flex;
flex: 0 1 auto;
margin: 0 10px 5px 0;
border: 1px solid $primary-low;
border-radius: 10px;
.label {
display: flex;
align-items: center;
color: $primary;
background: $primary-very-low;
justify-content: center;
border-radius: 9px 0 0 9px;
padding: 0 5px 0 8px;
.d-icon {
margin-right: 5px;
font-size: $font-down-1;
}
}
.value {
padding: 0 8px 0 5px;
}
&.user-newuser{
.label {
color: $primary-high;
}
}
&.user-basic , &.user-member {
border-color: $bronze;
.label {
border-color: $bronze;
background: $bronze;
color: $secondary;
}
}
&.user-regular {
border-color: $silver;
.label {
border-color: $silver;
background: $silver;
color: $secondary;
}
}
&.user-leader {
border-color: $gold;
.label {
background: $gold;
border-color: $gold;
color: $secondary;
}
}
}
}
.dashboard-inline-table {
margin-left: 5%;
margin-bottom: 1.25em;
.table-title {
border-bottom: 1px solid $primary-low;
margin-bottom: 1em;
}
.table-container {
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
}
}
.dashboard-table.activity-metrics {
table {
@media screen and (min-width: 400px) {
table-layout: auto;
}
tr th {
text-align: right;
}
.d-icon {
color: $primary-low-mid;
min-width: 14px;
text-align: center;
}
@media screen and (max-width: 400px) {
.d-icon { display: none; }
td.title {
padding: 8px 0 8px 4px;
}
}
}
}
.rtl .dashboard-next {
.section-column {
&:last-child, {
margin-right: 1em;
margin-left: 0;
}
&:first-child {
margin-left: 1em;
margin-right: 0;
}
}
.dashboard-table table tbody tr {
td.title {
text-align: right;
}
td.value i {
margin-right: 0;
margin-left: -12px;
}
}
.user-metrics .table-cell {
margin: 0 0 5px 10px;
}
.table-cell {
.label {
border-radius: 0 9px 9px 0;
.d-icon {
margin-right: 0;
margin-left: 5px;
}
}
}
}

View File

@ -3,19 +3,19 @@
.category-color-editor {
input {
width: 70px;
margin-right: 10px;
}
.color-title {
display: inline-block;
width: 130px;
width: 120px;
}
.colors-container {
display: inline-block;
vertical-align: middle;
padding-top: 4px;
padding-left: 15px;
max-width: 300px;
max-width: 240px;
.colorpicker {
border: 1px solid $primary-low;

View File

@ -13,6 +13,9 @@
}
#login-form {
table {
width: 100%;
}
a {
color: dark-light-choose($primary-high, $secondary-low);
}
@ -40,6 +43,12 @@ $input-width: 220px;
td {
label, input {
margin-bottom: 0;
width: 100%;
}
}
.modal-footer {
.inline-spinner {
display: inline-flex;
}
}
}
@ -56,9 +65,8 @@ $input-width: 220px;
}
.disclaimer {
font-size: $font-down-1;
color: dark-light-choose($primary-medium, $secondary-medium);
clear: both;
margin-top: .5em;
}
.user-field.confirm {
@ -106,12 +114,16 @@ $input-width: 220px;
.invites-show {
.two-col {
position: relative;
display: flex;
}
.col-image {
position: absolute;
top: 0;
left: 0;
position: relative;
width: 150px;
margin-right: 20px;
@media screen and (max-width: 600px) {
display: none;
}
}
form {
@ -135,6 +147,21 @@ $input-width: 220px;
}
}
.auth-message {
padding: 0 15px 15px 15px;
&:empty {
padding: 0;
}
}
.modal tr.instructions {
display: flex;
flex-direction: column;
margin-top: 0.15em;
label {
color: dark-light-choose($primary-medium, $secondary-medium);
}
}
// alternate login / create new account buttons should be de-emphasized
@ -142,4 +169,4 @@ button#login-link, button#new-account-link
{
background: transparent;
color: dark-light-choose($primary-high, $secondary-low);
}
}

View File

@ -76,8 +76,9 @@
float: left;
background-color: transparent;
display: inline-flex;
margin: 0.25em 0.5em;
width: 43%;
padding: 0.25em 0.5em;
width: 50%;
box-sizing: border-box;
.badge-notification {
color: dark-light-choose($primary-medium, $secondary-medium);
background-color: transparent;

View File

@ -31,9 +31,17 @@
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid $primary-low;
h3 {
margin-bottom: 0;
.title {
h3 {
margin-bottom: 0;
}
p {
margin: 0;
}
}
.modal-close {
order: 2;
margin-left: auto;
@ -80,12 +88,20 @@
}
.modal-inner-container {
display: table;
width: 100%;
min-width: 320px;
max-width: 700px;
margin: 0 auto;
background-color: $secondary;
background-clip: padding-box;
box-shadow: shadow("modal");
@media screen and (min-width: 475px) {
min-width: 475px;
width: auto;
}
.select-kit {
width: 220px;
@ -145,6 +161,11 @@
height: 10em;
}
}
@media screen and (min-width: 524px) {
.modal-inner-container {
min-width: 525px;
}
}
}
.modal {
@ -253,6 +274,11 @@
.modal-body.forgot-password-modal p {
font-size: $font-0;
}
pre code {
white-space: pre-wrap;
max-width: 700px;
}
}
.reply-where-modal {
@ -308,6 +334,7 @@
label {
margin-top: 7px;
max-width: 450px;
}
.optional {
@ -404,6 +431,8 @@
}
.incoming-email-content {
height: 300px;
max-width: 700px;
width: 90vw; // forcing textarea wider
textarea, .incoming-email-html-part {
height: 95%;
border: none;
@ -503,4 +532,3 @@
position: absolute;
width: 95%;
}

View File

@ -329,7 +329,7 @@ pre.onebox code li{
pre.onebox code ol.lines{
position:relative;
margin-left: 40px;
margin: 0 0 0 40px;
}
pre.onebox code ol.lines li {

View File

@ -20,15 +20,15 @@
}
&.badge-type-gold .fa {
color: rgb(231, 195, 0) !important;
color: $gold !important;
}
&.badge-type-silver .fa {
color: #c0c0c0 !important;
color: $silver !important;
}
&.badge-type-bronze .fa {
color: #cd7f32 !important;
color: $bronze !important;
}
&.disabled {

View File

@ -541,6 +541,11 @@
width: 520px;
}
.warning-wrap {
height: 30px;
margin-bottom: 10px;
}
.category-notifications .category-controls,
.tag-notifications .tag-controls {
margin-top: 24px;

View File

@ -1,5 +1,4 @@
.conditional-loading-section {
&.is-loading {
padding: 2em;
margin: 1em;

View File

@ -22,6 +22,13 @@ $twitter: #00bced !default;
$yahoo: #810293 !default;
$github: #6d6d6d !default;
// Badge color variables
// --------------------------------------------------
$gold: rgb(231, 195, 0) !default;
$silver: #c0c0c0 !default;
$bronze: #cd7f32 !default;
// Fonts
// --------------------------------------------------

View File

@ -58,6 +58,7 @@
.collection-header {
max-height: 125px;
overflow-y: auto;
flex: 1 0 auto;
.selected-tags {
display: flex;

View File

@ -1,104 +1,177 @@
// style that apply to the login popup
// shared styles used
// on both the login and
// create account modals
.login-modal,
.create-account {
#login-buttons:not(.hidden) {
display: flex;
flex-direction: column;
justify-content: center;
flex-basis: 40%;
align-items: center;
min-height: 175px;
border-left: 1px solid $primary-low;
padding: 15px;
order: 2;
#login-buttons {
button {
margin: 0 5px 5px 0;
min-width: 180px;
&:lang(zh_CN) {
min-width: 200px;
button {
margin: 0.35em;
width: 160px;
&:lang(zh_CN) {
min-width: 200px;
}
}
}
margin-top: 10px;
margin-bottom: 20px;
}
// Create account
#login-form {
flex: 1 0 auto;
padding: 0 15px;
}
.create-account {
form {
tr:not(.instructions) {
td {
display: flex;
padding: 5px 0 0 0;
}
}
tr.input label {
margin-bottom: 0;
}
table {
width: 100%;
}
tr.input {
td {
padding-top: 10px;
}
// styles used on
// login modal only
.d-modal.login-modal {
.modal-body,
#credentials {
display: flex;
align-items: center;
padding: 15px 0;
tr {
display: flex;
flex-direction: column;
}
input, label {
}
}
// styles used on the
// create account
// modal only
.d-modal.create-account {
.modal-body {
display: flex;
padding: 15px 0;
}
.create-account-form tr {
display: flex;
flex-direction: column;
}
.login-form {
display: flex;
flex: 1 1 50%;
align-items: center;
padding: 0 15px;
form, table {
width: 100%;
}
tr {
display: flex;
flex-direction: column;
margin-top: 0.15em;
&.password-confirmation {
display: none;
}
}
}
tr.input {
input,
label {
margin-bottom: 0;
width: 100%;
}
.tip {
max-width: 340px;
}
}
.invites-show {
padding-top: 20px;
.two-col {
margin-top: 30px;
}
.col-image {
width: 200px;
img {
width: 200px;
}
}
.col-form {
margin-left: 200px;
.inline-invite-img {
display: none;
}
}
form {
.controls,
.input {
margin-left: 20px;
}
input,
label {
margin-bottom: 0;
}
.user-field .control-label:not(.checkbox-label) {
margin-left: 20px;
}
}
}
.password-reset,
.invites-show {
.col-form {
padding-left: 20px;
}
h2 {
margin-bottom: 12px;
}
.col-image img {
width: 200px;
height: 200px;
}
}
.password-reset {
.col-form {
padding-top: 40px;
}
}
.tos-agree {
margin-bottom: 12px;
}
.disclaimer {
margin-top: 15px;
}
.instructions {
label {
color: dark-light-choose($primary-medium, $secondary-medium);
}
}
.user-fields {
border-top: 1px solid $primary-low;
padding-top: 20px;
}
}
.password-reset, .invites-show {
.col-form {
padding-left: 20px;
}
h2 {
margin-bottom: 12px;
}
.col-image img {
width: 200px;
height: 200px;
}
}
.password-reset {
.col-form {
padding-top: 40px;
}
}
.invites-show {
padding-top: 20px;
.two-col {
margin-top: 30px;
}
.col-image {
width: 200px;
img {
width: 200px;
}
}
.col-form {
margin-left: 200px;
.inline-invite-img {
.login-form {
.tip {
&:not(:empty) + td {
display: none;
}
}
form {
.controls, .input {
margin-left: 20px;
}
input, label {
margin-bottom: 0;
}
.user-field .control-label:not(.checkbox-label) {
margin-left: 20px;
&:not(:empty),
&:empty + td {
min-height: 1.75em;
width: 240px;
}
}
}
}

View File

@ -4,10 +4,16 @@
padding-left: 10px;
}
.radios {
height: 60px;
min-height: 60px;
display: flex;
align-items: flex-start;
label {
flex: 1 0 auto;
margin-right: 1em;
margin-top: 1px;
}
.inputs {
float: right;
width: 75%;
width: 100%;
input {
width: 90%;
margin: 0 0 5px 0;
@ -26,8 +32,8 @@
}
}
}
.radios:last-child {
height: 20px;
.radios:last-child:not(:nth-child(2)) { // last child for composer modal, but not theme import modal
min-height: 20px;
}
}

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