Version bump
This commit is contained in:
commit
7e69341dcd
@ -47,6 +47,7 @@ before_install:
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-staff-notes.git plugins/discourse-staff-notes
|
||||
- export PATH=$HOME/.yarn/bin:$PATH
|
||||
|
||||
install:
|
||||
|
||||
3
Gemfile
3
Gemfile
@ -34,7 +34,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.45'
|
||||
gem 'onebox', '1.8.46'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -116,7 +116,6 @@ group :test, :development do
|
||||
gem 'certified', require: false
|
||||
# later appears to break Fabricate(:topic, category: category)
|
||||
gem 'fabrication', '2.9.8', require: false
|
||||
gem 'discourse-qunit-rails', require: 'qunit-rails'
|
||||
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
|
||||
|
||||
19
Gemfile.lock
19
Gemfile.lock
@ -83,8 +83,6 @@ GEM
|
||||
crass (1.0.3)
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
discourse-qunit-rails (0.0.11)
|
||||
railties
|
||||
discourse_image_optim (0.24.5)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
@ -166,7 +164,7 @@ GEM
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
memory_profiler (0.9.10)
|
||||
message_bus (2.1.2)
|
||||
message_bus (2.1.4)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
@ -226,7 +224,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.45)
|
||||
onebox (1.8.46)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -289,9 +287,9 @@ GEM
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
trollop (>= 1.16.2)
|
||||
redis (3.3.5)
|
||||
redis-namespace (1.5.3)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
redis (4.0.1)
|
||||
redis-namespace (1.6.0)
|
||||
redis (>= 3.0.4)
|
||||
request_store (1.3.2)
|
||||
rinku (2.0.2)
|
||||
rotp (3.3.0)
|
||||
@ -355,11 +353,11 @@ GEM
|
||||
shoulda-context (1.2.2)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (5.0.5)
|
||||
sidekiq (5.1.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (>= 3.3.4, < 5)
|
||||
redis (>= 3.3.5, < 5)
|
||||
slop (3.6.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@ -412,7 +410,6 @@ DEPENDENCIES
|
||||
byebug
|
||||
certified
|
||||
cppjieba_rb
|
||||
discourse-qunit-rails
|
||||
discourse_image_optim
|
||||
email_reply_trimmer (= 0.1.11)
|
||||
ember-handlebars-template (= 0.7.5)
|
||||
@ -460,7 +457,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.45)
|
||||
onebox (= 1.8.46)
|
||||
openid-redis-store
|
||||
pg (~> 0.21.0)
|
||||
pry-nav
|
||||
|
||||
@ -83,7 +83,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
|
||||
|
||||
## Copyright / License
|
||||
|
||||
Copyright 2014 - 2017 Civilized Discourse Construction Kit, Inc.
|
||||
Copyright 2014 - 2018 Civilized Discourse Construction Kit, Inc.
|
||||
|
||||
Licensed under the GNU General Public License Version 2.0 (or later);
|
||||
you may not use this work except in compliance with the License.
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["dashboard-table", "dashboard-inline-table"],
|
||||
|
||||
classNameBindings: ["isLoading"],
|
||||
|
||||
total: null,
|
||||
labels: null,
|
||||
title: null,
|
||||
chartData: null,
|
||||
isLoading: false,
|
||||
help: null,
|
||||
helpPage: null,
|
||||
model: null,
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
if (this.get("dataSourceName")){
|
||||
this._fetchReport();
|
||||
} else if (this.get("model")) {
|
||||
this._setPropertiesFromModel(this.get("model"));
|
||||
}
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
if (this.get("model")) {
|
||||
this._setPropertiesFromModel(this.get("model"));
|
||||
}
|
||||
},
|
||||
|
||||
@computed("dataSourceName")
|
||||
dataSource(dataSourceName) {
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
},
|
||||
|
||||
_fetchReport() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
this.set("isLoading", true);
|
||||
|
||||
ajax(this.get("dataSource"))
|
||||
.then((response) => {
|
||||
this._setPropertiesFromModel(response.report);
|
||||
}).finally(() => {
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
},
|
||||
|
||||
_setPropertiesFromModel(model) {
|
||||
const data = model.data.sort((a, b) => a.x >= b.x);
|
||||
|
||||
this.setProperties({
|
||||
labels: data.map(r => r.x),
|
||||
dataset: data.map(r => r.y),
|
||||
total: model.total,
|
||||
title: model.title,
|
||||
chartData: data
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,171 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["dashboard-mini-chart"],
|
||||
|
||||
classNameBindings: ["trend", "oneDataPoint", "isLoading"],
|
||||
|
||||
isLoading: false,
|
||||
total: null,
|
||||
trend: null,
|
||||
title: null,
|
||||
oneDataPoint: false,
|
||||
backgroundColor: "rgba(200,220,240,0.3)",
|
||||
borderColor: "#08C",
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
this._initializeChart();
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
this._initializeChart();
|
||||
},
|
||||
|
||||
@computed("dataSourceName")
|
||||
dataSource(dataSourceName) {
|
||||
if (dataSourceName) {
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("trend")
|
||||
trendIcon(trend) {
|
||||
if (trend === "stable") {
|
||||
return null;
|
||||
} else {
|
||||
return `angle-${trend}`;
|
||||
}
|
||||
},
|
||||
|
||||
_fetchReport() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
this.set("isLoading", true);
|
||||
|
||||
let payload = {data: {}};
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").toISOString();
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = this.get("endDate").toISOString();
|
||||
}
|
||||
|
||||
ajax(this.get("dataSource"), payload)
|
||||
.then((response) => {
|
||||
this._setPropertiesFromModel(response.report);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("isLoading", false);
|
||||
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
if (!this.get("oneDataPoint")) {
|
||||
this._drawChart();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_initializeChart() {
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
if (this.get("model") && !this.get("values")) {
|
||||
this._setPropertiesFromModel(this.get("model"));
|
||||
this._drawChart();
|
||||
} else if (this.get("dataSource")) {
|
||||
this._fetchReport();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_drawChart() {
|
||||
const $chartCanvas = this.$(".chart-canvas");
|
||||
if (!$chartCanvas.length) return;
|
||||
|
||||
const context = $chartCanvas[0].getContext("2d");
|
||||
|
||||
const data = {
|
||||
labels: this.get("labels"),
|
||||
datasets: [{
|
||||
data: this.get("values"),
|
||||
backgroundColor: this.get("backgroundColor"),
|
||||
borderColor: this.get("borderColor")
|
||||
}]
|
||||
};
|
||||
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
},
|
||||
|
||||
_setPropertiesFromModel(model) {
|
||||
this.setProperties({
|
||||
labels: model.data.map(r => r.x),
|
||||
values: model.data.map(r => r.y),
|
||||
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
|
||||
this.get("startDate").isSame(this.get("endDate"), 'day'),
|
||||
total: model.total,
|
||||
title: model.title,
|
||||
trend: this._computeTrend(model.total, model.prev30Days)
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
const values = this.get("values");
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const stepSize = Math.max(...[Math.ceil((max - min)/5), 20]);
|
||||
|
||||
const startDate = this.get("startDate") || moment();
|
||||
const endDate = this.get("endDate") || moment();
|
||||
const datesDifference = startDate.diff(endDate, "days");
|
||||
let unit = "day";
|
||||
if (datesDifference >= 366) {
|
||||
unit = "quarter";
|
||||
} else if (datesDifference >= 61) {
|
||||
unit = "month";
|
||||
} else if (datesDifference >= 14) {
|
||||
unit = "week";
|
||||
}
|
||||
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
legend: { display: false },
|
||||
responsive: true,
|
||||
layout: { padding: { left: 0, top: 0, right: 0, bottom: 0 } },
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: { suggestedMin: 0, stepSize, suggestedMax: max + stepSize }
|
||||
}
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD",
|
||||
unit
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
_computeTrend(total, prevTotal) {
|
||||
const percentChange = ((total - prevTotal) / prevTotal) * 100;
|
||||
|
||||
if (percentChange > 50) return "double-up";
|
||||
if (percentChange > 0) return "up";
|
||||
if (percentChange === 0) return "stable";
|
||||
if (percentChange < 50) return "double-down";
|
||||
if (percentChange < 0) return "down";
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import DashboardTable from "admin/components/dashboard-table";
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
export default DashboardTable.extend({
|
||||
layoutName: "admin/templates/components/dashboard-table",
|
||||
|
||||
classNames: ["dashboard-table", "dashboard-table-trending-search"],
|
||||
|
||||
transformModel(model) {
|
||||
return {
|
||||
labels: model.labels,
|
||||
values: model.data.map(data => {
|
||||
return [data[0], number(data[1]), number(data[2])];
|
||||
})
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,83 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["dashboard-table"],
|
||||
|
||||
classNameBindings: ["isLoading"],
|
||||
|
||||
total: null,
|
||||
labels: null,
|
||||
title: null,
|
||||
chartData: null,
|
||||
isLoading: false,
|
||||
help: null,
|
||||
helpPage: null,
|
||||
model: null,
|
||||
|
||||
transformModel(model) {
|
||||
const data = model.data.sort((a, b) => a.x >= b.x);
|
||||
|
||||
return {
|
||||
labels: model.labels,
|
||||
values: data
|
||||
};
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
this._initializeTable();
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
this._initializeTable();
|
||||
},
|
||||
|
||||
@computed("dataSourceName")
|
||||
dataSource(dataSourceName) {
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
},
|
||||
|
||||
_initializeTable() {
|
||||
if (this.get("model") && !this.get("values")) {
|
||||
this._setPropertiesFromModel(this.get("model"));
|
||||
} else if (this.get("dataSource")) {
|
||||
this._fetchReport();
|
||||
}
|
||||
},
|
||||
|
||||
_fetchReport() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
this.set("isLoading", true);
|
||||
|
||||
let payload = {data: {}};
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").toISOString();
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = this.get("endDate").toISOString();
|
||||
}
|
||||
|
||||
ajax(this.get("dataSource"), payload)
|
||||
.then((response) => {
|
||||
this._setPropertiesFromModel(response.report);
|
||||
}).finally(() => {
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
},
|
||||
|
||||
_setPropertiesFromModel(model) {
|
||||
const { labels, values } = this.transformModel(model);
|
||||
|
||||
this.setProperties({
|
||||
labels,
|
||||
values,
|
||||
total: model.total,
|
||||
title: model.title
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,87 @@
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import AdminDashboardNext from 'admin/models/admin-dashboard-next';
|
||||
import Report from 'admin/models/report';
|
||||
|
||||
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"];
|
||||
|
||||
const REPORTS = [ "global_reports", "user_reports" ];
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["period"],
|
||||
period: "all",
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller('exception'),
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
AdminDashboardNext.find().then(d => {
|
||||
this.set("dashboardFetchedAt", new Date());
|
||||
|
||||
const reports = {};
|
||||
REPORTS.forEach(name => d[name].forEach(r => reports[`${name}_${r.type}`] = Report.create(r)));
|
||||
this.setProperties(reports);
|
||||
|
||||
ATTRIBUTES.forEach(a => this.set(a, d[a]));
|
||||
}).catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
this.replaceRoute("exception");
|
||||
}).finally(() => {
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return moment().subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return moment().subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return moment().subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return moment().subtract(1, "month").startOf("day");
|
||||
break;
|
||||
case "daily":
|
||||
return moment().startOf("day");
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
endDate(period) {
|
||||
return period === "all" ? null : moment().endOf("day");
|
||||
},
|
||||
|
||||
@computed("updated_at")
|
||||
updatedTimestamp(updatedAt) {
|
||||
return moment(updatedAt).format("LLL");
|
||||
},
|
||||
|
||||
@computed("last_backup_taken_at")
|
||||
backupTimestamp(lastBackupTakenAt) {
|
||||
return moment(lastBackupTakenAt).format("LLL");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
}
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return `/admin/dashboard-next?period=${period}`;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
const AdminDashboardNext = Discourse.Model.extend({});
|
||||
|
||||
AdminDashboardNext.reopenClass({
|
||||
|
||||
/**
|
||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||
has expired and the server must collect the data again.
|
||||
|
||||
@method find
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
find: function() {
|
||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||
var model = AdminDashboardNext.create(json);
|
||||
model.set('loaded', true);
|
||||
return model;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminDashboardNext;
|
||||
@ -62,11 +62,74 @@ export default Post.extend({
|
||||
},
|
||||
|
||||
deferFlags(deletePost) {
|
||||
return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }).catch(popupAjaxError);
|
||||
const action = () => {
|
||||
return ajax('/admin/flags/defer/' + this.id, {
|
||||
type: 'POST', cache: false, data: { delete_post: deletePost }
|
||||
});
|
||||
};
|
||||
|
||||
if (deletePost && this._hasDeletableReplies()) {
|
||||
return this._actOnFlagAndDeleteReplies(action);
|
||||
} else {
|
||||
return action().catch(popupAjaxError);
|
||||
}
|
||||
},
|
||||
|
||||
agreeFlags(actionOnPost) {
|
||||
return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }).catch(popupAjaxError);
|
||||
const action = () => {
|
||||
return ajax('/admin/flags/agree/' + this.id, {
|
||||
type: 'POST', cache: false, data: { action_on_post: actionOnPost }
|
||||
});
|
||||
};
|
||||
|
||||
if (actionOnPost === 'delete' && this._hasDeletableReplies()) {
|
||||
return this._actOnFlagAndDeleteReplies(action);
|
||||
} else {
|
||||
return action().catch(popupAjaxError);
|
||||
}
|
||||
},
|
||||
|
||||
_hasDeletableReplies() {
|
||||
return this.get('post_number') > 1 && this.get('reply_count') > 0;
|
||||
},
|
||||
|
||||
_actOnFlagAndDeleteReplies(action) {
|
||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||
return ajax(`/posts/${this.id}/reply-ids/all.json`).then(replies => {
|
||||
const buttons = [];
|
||||
|
||||
buttons.push({
|
||||
label: I18n.t('no_value'),
|
||||
callback() {
|
||||
action()
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
label: I18n.t('yes_value'),
|
||||
class: "btn-danger",
|
||||
callback() {
|
||||
Post.deleteMany(replies.map(r => r.id))
|
||||
.then(action)
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
bootbox.dialog(I18n.t("admin.flags.delete_replies", { count: replies.length }), buttons);
|
||||
}).catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
postHidden: Ember.computed.alias('hidden'),
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor('admin-dashboard-next').fetchDashboard();
|
||||
}
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
export default function() {
|
||||
this.route('admin', { resetNamespace: true }, function() {
|
||||
this.route('dashboard', { path: '/' });
|
||||
this.route('dashboardNext', { path: '/dashboard-next' });
|
||||
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
|
||||
this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} );
|
||||
});
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
|
||||
{{#if help}}
|
||||
<a href="{{helpPage}}">{{i18n help}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{{#each labels as |label|}}
|
||||
<th>{{label}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#each dataset as |data|}}
|
||||
<td>{{number data}}</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
@ -0,0 +1,26 @@
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
<div class="chart-title">
|
||||
<h3>{{title}}</h3>
|
||||
|
||||
{{#if help}}
|
||||
{{d-icon "question-circle" title=help}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
{{#if oneDataPoint}}
|
||||
<span class="data-point">
|
||||
{{number chartData.lastObject.y}}
|
||||
</span>
|
||||
{{else}}
|
||||
<div class="chart-trend {{trend}}">
|
||||
<span>{{number total}}</span>
|
||||
|
||||
{{#if trendIcon}}
|
||||
{{d-icon trendIcon}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<canvas class="chart-canvas"></canvas>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
@ -0,0 +1,31 @@
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
<div class="table-title">
|
||||
<h3>{{title}}</h3>
|
||||
|
||||
{{#if help}}
|
||||
<a href="{{helpPage}}">{{i18n help}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{{#each labels as |label|}}
|
||||
<th>{{label}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each values as |value|}}
|
||||
<tr>
|
||||
{{#each value as |v|}}
|
||||
<td>{{v}}</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
82
app/assets/javascripts/admin/templates/dashboard_next.hbs
Normal file
82
app/assets/javascripts/admin/templates/dashboard_next.hbs
Normal file
@ -0,0 +1,82 @@
|
||||
{{plugin-outlet name="admin-dashboard-top"}}
|
||||
{{lastRefreshedAt}}
|
||||
<div class="community-health section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
{{period-chooser period=period action="changePeriod"}}
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{dashboard-mini-chart
|
||||
model=global_reports_signups
|
||||
dataSourceName="signups"
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
help="admin.dashboard.charts.signups.help"}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
model=global_reports_topics
|
||||
dataSourceName="topics"
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
help="admin.dashboard.charts.topics.help"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<div class="section-column">
|
||||
{{dashboard-inline-table
|
||||
model=user_reports_users_by_type
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
isLoading=isLoading}}
|
||||
|
||||
{{dashboard-inline-table
|
||||
model=user_reports_users_by_trust_level
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
isLoading=isLoading}}
|
||||
|
||||
{{#conditional-loading-spinner isLoading=isLoading}}
|
||||
<div class="misc">
|
||||
<div class="durability">
|
||||
{{#if currentUser.admin}}
|
||||
<div class="backups">
|
||||
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
||||
<p>
|
||||
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}})
|
||||
<br />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="uploads">
|
||||
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<p>
|
||||
{{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="last-dashboard-update">
|
||||
{{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}}
|
||||
</p>
|
||||
|
||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/t/discourse-2-0-0-beta6-release-notes/85241" class="btn">
|
||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||
</a>
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
|
||||
<div class="section-column">
|
||||
{{dashboard-table-trending-search
|
||||
model=global_reports_trending_search
|
||||
dataSourceName="trending_search"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
@ -64,6 +64,7 @@
|
||||
//= require ./discourse/models/draft
|
||||
//= require ./discourse/models/composer
|
||||
//= require ./discourse/models/user-badge
|
||||
//= require_tree ./discourse/lib
|
||||
//= require_tree ./discourse/mixins
|
||||
//= require ./discourse/models/invite
|
||||
//= require ./discourse/controllers/discovery-sortable
|
||||
@ -87,7 +88,6 @@
|
||||
//= require ./discourse/helpers/loading-spinner
|
||||
//= require ./discourse/helpers/category-link
|
||||
//= require ./discourse/lib/export-result
|
||||
//= require_tree ./discourse/lib
|
||||
//= require ./discourse/mapping-router
|
||||
|
||||
//= require_tree ./discourse/controllers
|
||||
|
||||
@ -9,7 +9,7 @@ const REPLACEMENTS = {
|
||||
'd-watching-first': 'dot-circle-o',
|
||||
'd-drop-expanded': 'caret-down',
|
||||
'd-drop-collapsed': 'caret-right',
|
||||
'd-unliked': 'heart',
|
||||
'd-unliked': 'heart-o',
|
||||
'd-liked': 'heart',
|
||||
'notification.mentioned': "at",
|
||||
'notification.group_mentioned': "at",
|
||||
|
||||
@ -1 +1,4 @@
|
||||
export default Ember.Component.extend({ tagName: '' });
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
label: 'topic.create'
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import { siteDir } from 'discourse/lib/text-direction';
|
||||
import { determinePostReplaceSelection, clipboardData } from 'discourse/lib/utilities';
|
||||
import toMarkdown from 'discourse/lib/to-markdown';
|
||||
import deprecated from 'discourse-common/lib/deprecated';
|
||||
import { wantsNewWindow } from 'discourse/lib/intercept-click';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
@ -258,7 +259,15 @@ export default Ember.Component.extend({
|
||||
|
||||
// disable clicking on links in the preview
|
||||
this.$('.d-editor-preview').on('click.preview', e => {
|
||||
if ($(e.target).is("a")) {
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
const $target = $(e.target);
|
||||
if ($target.is("a.mention")) {
|
||||
this.appEvents.trigger('click.discourse-preview-user-card-mention', $target);
|
||||
}
|
||||
if ($target.is("a.mention-group")) {
|
||||
this.appEvents.trigger('click.discourse-preview-group-card-mention-group', $target);
|
||||
}
|
||||
if ($target.is("a")) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -13,6 +13,12 @@ export default Ember.Component.extend({
|
||||
return this.site.get('categoriesList');
|
||||
},
|
||||
|
||||
@computed('hasDraft')
|
||||
createTopicLabel(hasDraft)
|
||||
{
|
||||
return hasDraft ? 'topic.open_draft': 'topic.create';
|
||||
},
|
||||
|
||||
@computed('category.can_edit')
|
||||
showCategoryEdit: canEdit => canEdit,
|
||||
|
||||
|
||||
@ -9,6 +9,11 @@ export default Ember.Component.extend(bufferedRender({
|
||||
buildBuffer(buffer) {
|
||||
let notices = [];
|
||||
|
||||
if ($.cookie("dosp") === "1") {
|
||||
$.cookie("dosp", null, { path: '/' });
|
||||
notices.push([I18n.t("forced_anonymous"), 'forced-anonymous']);
|
||||
}
|
||||
|
||||
if (this.session.get('safe_mode')) {
|
||||
notices.push([I18n.t("safe_mode.enabled"), 'safe-mode']);
|
||||
}
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import CardContentsBase from 'discourse/mixins/card-contents-base';
|
||||
import CleansUp from 'discourse/mixins/cleans-up';
|
||||
|
||||
const maxMembersToDisplay = 10;
|
||||
|
||||
export default Ember.Component.extend(CardContentsBase, CleansUp, {
|
||||
elementId: 'group-card',
|
||||
triggeringLinkClass: 'mention-group',
|
||||
classNames: ['no-bg'],
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'isFixed:fixed'],
|
||||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
showBadges: setting('enable_badges'),
|
||||
|
||||
postStream: Ember.computed.alias('topic.postStream'),
|
||||
viewingTopic: Ember.computed.match('currentPath', /^topic\./),
|
||||
|
||||
showMoreMembers: Ember.computed.gt('moreMembersCount', 0),
|
||||
|
||||
group: null,
|
||||
|
||||
@computed('group.user_count', 'group.members.length')
|
||||
moreMembersCount: (memberCount, maxMemberDisplay) => memberCount - maxMemberDisplay,
|
||||
|
||||
@computed('group')
|
||||
groupPath(group) {
|
||||
return `${Discourse.BaseUri}/groups/${group.name}`;
|
||||
},
|
||||
|
||||
_showCallback(username, $target) {
|
||||
this.store.find("group", username).then(group => {
|
||||
this.setProperties({ group, visible: true });
|
||||
this._positionCard($target);
|
||||
if(!group.flair_url && !group.flair_bg_color) {
|
||||
group.set('flair_url', 'fa-users');
|
||||
}
|
||||
group.set('limit', maxMembersToDisplay);
|
||||
return group.findMembers();
|
||||
}).catch(() => this._close()).finally(() => this.set('loading', null));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
},
|
||||
|
||||
_close() {
|
||||
this._super();
|
||||
this.setProperties({
|
||||
group: null,
|
||||
});
|
||||
},
|
||||
|
||||
cleanUp() {
|
||||
this._close();
|
||||
},
|
||||
|
||||
actions: {
|
||||
close() {
|
||||
this._close();
|
||||
},
|
||||
|
||||
cancelFilter() {
|
||||
const postStream = this.get('postStream');
|
||||
postStream.cancelFilter();
|
||||
postStream.refresh();
|
||||
this._close();
|
||||
},
|
||||
|
||||
composePrivateMessage(...args) {
|
||||
this.sendAction('composePrivateMessage', ...args);
|
||||
},
|
||||
|
||||
messageGroup() {
|
||||
this.sendAction('createNewMessageViaParams', this.get('group.name'));
|
||||
},
|
||||
|
||||
showGroup() {
|
||||
this.sendAction('showGroup', this.get('group'));
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -6,7 +6,7 @@ import { applySearchAutocomplete } from "discourse/lib/search";
|
||||
export default TextField.extend({
|
||||
@computed('searchService.searchContextEnabled')
|
||||
placeholder(searchContextEnabled) {
|
||||
return searchContextEnabled ? "" : I18n.t('search.title');
|
||||
return searchContextEnabled ? "" : I18n.t('search.full_page_title');
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
|
||||
@ -116,51 +116,31 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_dock() {
|
||||
const $topicProgressWrapper = this.$();
|
||||
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) return;
|
||||
const $wrapper = this.$();
|
||||
if (!$wrapper || $wrapper.length === 0) return;
|
||||
|
||||
// on desktop, we want the topic-progress after the last post
|
||||
// on mobile, we want it right before the end of the last post
|
||||
const progressHeight = this.site.mobileView ? 0 : $("#topic-progress").outerHeight();
|
||||
const offset = window.pageYOffset || $("html").scrollTop();
|
||||
const progressHeight = this.site.mobileView ? 0 : $("#topic-progress").height();
|
||||
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
|
||||
const windowHeight = $(window).height();
|
||||
const composerHeight = $("#reply-control").height() || 0;
|
||||
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;
|
||||
const bottom = $("#main").height() - maximumOffset;
|
||||
|
||||
const maximumOffset = $('#topic-bottom').offset();
|
||||
const composerHeight = $('#reply-control').height() || 0;
|
||||
const offset = window.pageYOffset || $('html').scrollTop();
|
||||
|
||||
const $replyArea = $('#reply-control .reply-area');
|
||||
if ($replyArea && $replyArea.length) {
|
||||
$topicProgressWrapper.css('right', `${$replyArea.offset().left}px`);
|
||||
} else {
|
||||
$topicProgressWrapper.css('right', `1em`);
|
||||
}
|
||||
|
||||
let isDocked = false;
|
||||
if (maximumOffset) {
|
||||
const threshold = maximumOffset.top + progressHeight;
|
||||
const windowHeight = $(window).height();
|
||||
|
||||
if (this.capabilities.isIOS) {
|
||||
const headerHeight = $('header').outerHeight(true);
|
||||
isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight);
|
||||
} else {
|
||||
isDocked = offset >= (threshold - windowHeight + composerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const dockPos = $(document).height() - maximumOffset.top - progressHeight;
|
||||
if (composerHeight > 0) {
|
||||
if (isDocked) {
|
||||
$topicProgressWrapper.css('bottom', dockPos);
|
||||
} else {
|
||||
const height = composerHeight + "px";
|
||||
if ($topicProgressWrapper.css('bottom') !== height) {
|
||||
$topicProgressWrapper.css('bottom', height);
|
||||
}
|
||||
}
|
||||
$wrapper.css("bottom", isDocked ? bottom : composerHeight);
|
||||
} else {
|
||||
$topicProgressWrapper.css('bottom', isDocked ? dockPos : '');
|
||||
$wrapper.css("bottom", isDocked ? bottom : "");
|
||||
}
|
||||
|
||||
this.set("docked", isDocked);
|
||||
|
||||
const $replyArea = $("#reply-control .reply-area");
|
||||
if ($replyArea && $replyArea.length > 0) {
|
||||
$wrapper.css("right", `${$replyArea.offset().left}px`);
|
||||
} else {
|
||||
$wrapper.css("right", "1em");
|
||||
}
|
||||
this.set('docked', isDocked);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
@ -169,7 +149,6 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
actions: {
|
||||
toggleExpansion() {
|
||||
this.toggleProperty('expanded');
|
||||
|
||||
@ -1,45 +1,31 @@
|
||||
import { wantsNewWindow } from 'discourse/lib/intercept-click';
|
||||
import { propertyNotEqual, setting } from 'discourse/lib/computed';
|
||||
import CleansUp from 'discourse/mixins/cleans-up';
|
||||
import afterTransition from 'discourse/lib/after-transition';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import User from 'discourse/models/user';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
import { propertyNotEqual, setting } from 'discourse/lib/computed';
|
||||
import { durationTiny } from 'discourse/lib/formatter';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import CardContentsBase from 'discourse/mixins/card-contents-base';
|
||||
import CleansUp from 'discourse/mixins/cleans-up';
|
||||
|
||||
const clickOutsideEventName = "mousedown.outside-user-card";
|
||||
const clickDataExpand = "click.discourse-user-card";
|
||||
const clickMention = "click.discourse-user-mention";
|
||||
|
||||
export default Ember.Component.extend(CleansUp, CanCheckEmails, {
|
||||
export default Ember.Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||
elementId: 'user-card',
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg'],
|
||||
triggeringLinkClass: 'mention',
|
||||
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage', 'user.card_background::no-bg', 'isFixed:fixed'],
|
||||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
showBadges: setting('enable_badges'),
|
||||
|
||||
postStream: Ember.computed.alias('topic.postStream'),
|
||||
enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2),
|
||||
viewingTopic: Ember.computed.match('currentPath', /^topic\./),
|
||||
viewingAdmin: Ember.computed.match('currentPath', /^admin\./),
|
||||
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'),
|
||||
showBadges: setting('enable_badges'),
|
||||
showMoreBadges: Ember.computed.gt('moreBadgesCount', 0),
|
||||
showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
|
||||
linkWebsite: Ember.computed.not('user.isBasic'),
|
||||
hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'),
|
||||
showCheckEmail: Ember.computed.and('user.staged', 'canCheckEmails'),
|
||||
|
||||
visible: false,
|
||||
user: null,
|
||||
username: null,
|
||||
avatar: null,
|
||||
userLoading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
|
||||
// If inside a topic
|
||||
topicPostCount: null,
|
||||
@ -75,21 +61,6 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
|
||||
@computed('user.badge_count', 'user.featured_user_badges.length')
|
||||
moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength,
|
||||
|
||||
@computed('user.card_badge.image')
|
||||
hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0,
|
||||
|
||||
@observes('user.card_background')
|
||||
addBackground() {
|
||||
if (!this.get('allowBackgrounds')) { return; }
|
||||
|
||||
const $this = this.$();
|
||||
if (!$this) { return; }
|
||||
|
||||
const url = this.get('user.card_background');
|
||||
const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`;
|
||||
$this.css('background-image', bg);
|
||||
},
|
||||
|
||||
@computed('user.time_read', 'user.recent_time_read')
|
||||
showRecentTimeRead(timeRead, recentTimeRead) {
|
||||
return timeRead !== recentTimeRead && recentTimeRead !== 0;
|
||||
@ -109,144 +80,43 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
|
||||
}
|
||||
},
|
||||
|
||||
_show(username, $target) {
|
||||
// No user card for anon
|
||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
|
||||
return false;
|
||||
}
|
||||
@observes('user.card_background')
|
||||
addBackground() {
|
||||
if (!this.get('allowBackgrounds')) { return; }
|
||||
|
||||
username = Ember.Handlebars.Utils.escapeExpression(username.toString());
|
||||
const $this = this.$();
|
||||
if (!$this) { return; }
|
||||
|
||||
// Don't show on mobile
|
||||
if (this.site.mobileView) {
|
||||
DiscourseURL.routeTo(userPath(username));
|
||||
return false;
|
||||
}
|
||||
const url = this.get('user.card_background');
|
||||
const bg = Ember.isEmpty(url) ? '' : `url(${Discourse.getURLWithCDN(url)})`;
|
||||
$this.css('background-image', bg);
|
||||
},
|
||||
|
||||
const currentUsername = this.get('username');
|
||||
if (username === currentUsername && this.get('userLoading') === username) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postId = $target.parents('article').data('post-id');
|
||||
|
||||
const wasVisible = this.get('visible');
|
||||
const previousTarget = this.get('cardTarget');
|
||||
const target = $target[0];
|
||||
if (wasVisible) {
|
||||
this._close();
|
||||
if (target === previousTarget) { return; }
|
||||
}
|
||||
|
||||
const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
|
||||
this.setProperties({ username, userLoading: username, cardTarget: target, post });
|
||||
@computed('user.card_badge.image')
|
||||
hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0,
|
||||
|
||||
_showCallback(username, $target) {
|
||||
const args = { stats: false };
|
||||
args.include_post_count_for = this.get('topic.id');
|
||||
|
||||
User.findByUsername(username, args).then(user => {
|
||||
if (user.topic_post_count) {
|
||||
this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
|
||||
}
|
||||
this.setProperties({ user, avatar: user, visible: true });
|
||||
|
||||
this._positionCard($target);
|
||||
}).catch(() => this._close()).finally(() => this.set('userLoading', null));
|
||||
this.setProperties({ user, visible: true });
|
||||
|
||||
return false;
|
||||
}).catch(() => this._close()).finally(() => this.set('loading', null));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
afterTransition(this.$(), this._hide.bind(this));
|
||||
|
||||
$('html').off(clickOutsideEventName)
|
||||
.on(clickOutsideEventName, (e) => {
|
||||
if (this.get('visible')) {
|
||||
const $target = $(e.target);
|
||||
if ($target.closest('[data-user-card]').data('userCard') ||
|
||||
$target.closest('a.mention').length > 0 ||
|
||||
$target.closest('#user-card').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._close();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => {
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
const $target = $(e.currentTarget);
|
||||
return this._show($target.data('user-card'), $target);
|
||||
});
|
||||
|
||||
$('#main-outlet').on(clickMention, 'a.mention', (e) => {
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
const $target = $(e.target);
|
||||
return this._show($target.text().replace(/^@/, ''), $target);
|
||||
});
|
||||
},
|
||||
|
||||
_positionCard(target) {
|
||||
const rtl = ($('html').css('direction')) === 'rtl';
|
||||
if (!target) { return; }
|
||||
const width = this.$().width();
|
||||
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
if (target) {
|
||||
let position = target.offset();
|
||||
if (position) {
|
||||
|
||||
if (rtl) { // The site direction is rtl
|
||||
position.right = $(window).width() - position.left + 10;
|
||||
position.left = 'auto';
|
||||
let overage = ($(window).width() - 50) - (position.right + width);
|
||||
if (overage < 0) {
|
||||
position.right += overage;
|
||||
position.top += target.height() + 48;
|
||||
}
|
||||
} else { // The site direction is ltr
|
||||
position.left += target.width() + 10;
|
||||
|
||||
let overage = ($(window).width() - 50) - (position.left + width);
|
||||
if (overage < 0) {
|
||||
position.left += overage;
|
||||
position.top += target.height() + 48;
|
||||
}
|
||||
}
|
||||
|
||||
position.top -= $('#main-outlet').offset().top;
|
||||
this.$().css(position);
|
||||
}
|
||||
|
||||
// After the card is shown, focus on the first link
|
||||
//
|
||||
// note: we DO NOT use afterRender here cause _positionCard may
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
Ember.run.next(null, () => this.$('a:first').focus() );
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_hide() {
|
||||
if (!this.get('visible')) {
|
||||
this.$().css({left: -9999, top: -9999});
|
||||
}
|
||||
},
|
||||
|
||||
_close() {
|
||||
this._super();
|
||||
this.setProperties({
|
||||
visible: false,
|
||||
user: null,
|
||||
username: null,
|
||||
avatar: null,
|
||||
userLoading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
topicPostCount: null
|
||||
topicPostCount: null,
|
||||
});
|
||||
},
|
||||
|
||||
@ -254,20 +124,6 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
|
||||
this._close();
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.keyCode === 27) { // ESC
|
||||
const target = this.get('cardTarget');
|
||||
this._close();
|
||||
target.focus();
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
$('html').off(clickOutsideEventName);
|
||||
$('#main').off(clickDataExpand).off(clickMention);
|
||||
},
|
||||
|
||||
actions: {
|
||||
close() {
|
||||
this._close();
|
||||
|
||||
@ -33,7 +33,8 @@ export default TextField.extend({
|
||||
excludeCurrentUser = bool('excludeCurrentUser'),
|
||||
single = bool('single'),
|
||||
allowAny = bool('allowAny'),
|
||||
disabled = bool('disabled');
|
||||
disabled = bool('disabled'),
|
||||
disallowEmails = bool('disallowEmails');
|
||||
|
||||
function excludedUsernames() {
|
||||
// hack works around some issues with allowAny eventing
|
||||
@ -64,7 +65,8 @@ export default TextField.extend({
|
||||
allowedUsers,
|
||||
includeMentionableGroups,
|
||||
includeMessageableGroups,
|
||||
group: self.get("group")
|
||||
group: self.get("group"),
|
||||
disallowEmails,
|
||||
});
|
||||
|
||||
return results;
|
||||
|
||||
@ -41,7 +41,8 @@ function loadDraft(store, opts) {
|
||||
composerState: Composer.DRAFT,
|
||||
composerTime: draft.composerTime,
|
||||
typingTime: draft.typingTime,
|
||||
whisper: draft.whisper
|
||||
whisper: draft.whisper,
|
||||
tags: draft.tags
|
||||
});
|
||||
return composer;
|
||||
}
|
||||
@ -682,7 +683,7 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
if (opts.topicTitle && opts.topicTitle.length <= this.siteSettings.max_topic_title_length) {
|
||||
this.set('model.title', opts.topicTitle);
|
||||
this.set('model.title', escapeExpression(opts.topicTitle));
|
||||
}
|
||||
|
||||
if (opts.topicCategoryId) {
|
||||
@ -707,7 +708,12 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
if (opts.topicTags && !this.site.mobileView && this.site.get('can_tag_topics')) {
|
||||
this.set('model.tags', opts.topicTags.split(","));
|
||||
const self = this;
|
||||
let tags = escapeExpression(opts.topicTags).split(",").slice(0, self.siteSettings.max_tags_per_topic);
|
||||
tags.forEach(function(tag, index, array) {
|
||||
array[index] = tag.substring(0, self.siteSettings.max_tag_length);
|
||||
});
|
||||
self.set('model.tags', tags);
|
||||
}
|
||||
|
||||
if (opts.topicBody) {
|
||||
@ -725,25 +731,26 @@ export default Ember.Controller.extend({
|
||||
destroyDraft() {
|
||||
const key = this.get('model.draftKey');
|
||||
if (key) {
|
||||
if (key === 'new_topic') {
|
||||
this.send('clearTopicDraft');
|
||||
}
|
||||
Draft.clear(key, this.get('model.draftSequence'));
|
||||
}
|
||||
},
|
||||
|
||||
cancelComposer() {
|
||||
const self = this;
|
||||
|
||||
return new Ember.RSVP.Promise(function (resolve) {
|
||||
if (self.get('model.hasMetaData') || self.get('model.replyDirty')) {
|
||||
return new Ember.RSVP.Promise((resolve) => {
|
||||
if (this.get('model.hasMetaData') || this.get('model.replyDirty')) {
|
||||
bootbox.dialog(I18n.t("post.abandon.confirm"), [
|
||||
{ label: I18n.t("post.abandon.no_value") },
|
||||
{
|
||||
label: I18n.t("post.abandon.yes_value"),
|
||||
'class': 'btn-danger',
|
||||
callback(result) {
|
||||
callback: (result) => {
|
||||
if (result) {
|
||||
self.destroyDraft();
|
||||
self.get('model').clearState();
|
||||
self.close();
|
||||
this.destroyDraft();
|
||||
this.get('model').clearState();
|
||||
this.close();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
@ -751,9 +758,9 @@ export default Ember.Controller.extend({
|
||||
]);
|
||||
} else {
|
||||
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
||||
self.destroyDraft();
|
||||
self.get('model').clearState();
|
||||
self.close();
|
||||
this.destroyDraft();
|
||||
this.get('model').clearState();
|
||||
this.close();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,6 +3,8 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { propertyGreaterThan, propertyLessThan } from 'discourse/lib/computed';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as WhiteLister } from 'pretty-text/white-lister';
|
||||
import { sanitize } from 'pretty-text/sanitizer';
|
||||
|
||||
function customTagArray(fieldName) {
|
||||
return function() {
|
||||
@ -187,7 +189,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
@computed('viewMode', 'model.body_changes')
|
||||
bodyDiff(viewMode) {
|
||||
return this.get("model.body_changes." + viewMode);
|
||||
const html = this.get(`model.body_changes.${viewMode}`);
|
||||
if (viewMode === "side_by_side_markdown") {
|
||||
return html;
|
||||
} else {
|
||||
const whiteLister = new WhiteLister({ features: { editHistory: true }});
|
||||
whiteLister.whiteListFeature("editHistory", { custom: () => true });
|
||||
return sanitize(html, whiteLister);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import NavigationDefaultController from 'discourse/controllers/navigation/default';
|
||||
|
||||
export default NavigationDefaultController.extend();
|
||||
export default NavigationDefaultController.extend({
|
||||
|
||||
discoveryCategories: Ember.inject.controller('discovery/categories'),
|
||||
|
||||
draft: function() {
|
||||
return this.get('discoveryCategories.model.draft');
|
||||
}.property('discoveryCategories.model', 'discoveryCategories.model.draft')
|
||||
});
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
discovery: Ember.inject.controller(),
|
||||
discoveryTopics: Ember.inject.controller('discovery/topics'),
|
||||
|
||||
draft: function() {
|
||||
return this.get('discoveryTopics.model.draft');
|
||||
}.property('discoveryTopics.model', 'discoveryTopics.model.draft')
|
||||
});
|
||||
|
||||
@ -61,6 +61,10 @@ export default Ember.Controller.extend(BulkTopicSelection, {
|
||||
|
||||
categories: Ember.computed.alias('site.categoriesList'),
|
||||
|
||||
createTopicLabel: function() {
|
||||
return this.get('list.draft') ? 'topic.open_draft' : 'topic.create';
|
||||
}.property('list', 'list.draft'),
|
||||
|
||||
@computed('canCreateTopic', 'category', 'canCreateTopicOnCategory')
|
||||
createTopicDisabled(canCreateTopic, category, canCreateTopicOnCategory) {
|
||||
return !canCreateTopic || (category && !canCreateTopicOnCategory);
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
stopNotificiationsText: function() {
|
||||
return I18n.t("topic.unsubscribe.stop_notifications", { title: this.get("model.fancyTitle") });
|
||||
}.property("model.fancyTitle"),
|
||||
|
||||
});
|
||||
@ -1,5 +1,20 @@
|
||||
// Initialize the message bus to receive messages.
|
||||
import pageVisible from 'discourse/lib/page-visible';
|
||||
import { handleLogoff } from 'discourse/lib/ajax';
|
||||
|
||||
function ajax(opts) {
|
||||
if (opts.complete) {
|
||||
let oldComplete = opts.complete;
|
||||
opts.complete = function(xhr, stat) {
|
||||
handleLogoff(xhr);
|
||||
oldComplete(xhr, stat);
|
||||
};
|
||||
} else {
|
||||
opts.complete = handleLogoff;
|
||||
}
|
||||
|
||||
return $.ajax(opts);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "message-bus",
|
||||
@ -41,7 +56,7 @@ export default {
|
||||
if (pageVisible()) {
|
||||
opts.headers['Discourse-Visible'] = "true";
|
||||
}
|
||||
return $.ajax(opts);
|
||||
return ajax(opts);
|
||||
};
|
||||
} else {
|
||||
|
||||
@ -50,7 +65,7 @@ export default {
|
||||
if (pageVisible()) {
|
||||
opts.headers['Discourse-Visible'] = "true";
|
||||
}
|
||||
return $.ajax(opts);
|
||||
return ajax(opts);
|
||||
};
|
||||
|
||||
messageBus.baseUrl = Discourse.getURL('/');
|
||||
|
||||
@ -13,6 +13,22 @@ export function viewTrackingRequired() {
|
||||
_trackView = true;
|
||||
}
|
||||
|
||||
export function handleLogoff(xhr) {
|
||||
if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) {
|
||||
_showingLogout = true;
|
||||
const messageBus = Discourse.__container__.lookup('message-bus:main');
|
||||
messageBus.stop();
|
||||
bootbox.dialog(
|
||||
I18n.t("logout"), {label: I18n.t("refresh"), callback: logout},
|
||||
{
|
||||
onEscape: () => logout(),
|
||||
backdrop: 'static'
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
Our own $.ajax method. Makes sure the .then method executes in an Ember runloop
|
||||
for performance reasons. Also automatically adjusts the URL to support installs
|
||||
@ -60,19 +76,6 @@ export function ajax() {
|
||||
args.headers['Discourse-Visible'] = "true";
|
||||
}
|
||||
|
||||
let handleLogoff = function(xhr) {
|
||||
if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) {
|
||||
_showingLogout = true;
|
||||
bootbox.dialog(
|
||||
I18n.t("logout"), {label: I18n.t("refresh"), callback: logout},
|
||||
{
|
||||
onEscape: () => logout(),
|
||||
backdrop: 'static'
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
args.success = (data, textStatus, xhr) => {
|
||||
handleLogoff(xhr);
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export function transformBasicPost(post) {
|
||||
deleted: post.get('deleted'),
|
||||
deleted_at: post.deleted_at,
|
||||
user_deleted: post.user_deleted,
|
||||
isDeleted: post.deleted_at || post.user_deleted,
|
||||
isDeleted: post.deleted_at || post.user_deleted, // xxxxx
|
||||
deletedByAvatarTemplate: null,
|
||||
deletedByUsername: null,
|
||||
primary_group_name: post.primary_group_name,
|
||||
@ -215,7 +215,8 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos
|
||||
postAtts.expandablePost = topic.expandable_first_post;
|
||||
} else {
|
||||
postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover;
|
||||
postAtts.canDelete = !postAtts.isDeleted && postAtts.canDelete;
|
||||
postAtts.canDelete = postAtts.canDelete && !post.deleted_at &&
|
||||
currentUser && (currentUser.staff || !post.user_deleted);
|
||||
}
|
||||
|
||||
_additionalAttributes.forEach(a => postAtts[a] = post[a]);
|
||||
|
||||
@ -61,7 +61,7 @@ function organizeResults(r, options) {
|
||||
});
|
||||
}
|
||||
|
||||
if (options.term.match(/@/)) {
|
||||
if (!options.disallowEmails && options.term.match(/@/)) {
|
||||
let e = { username: options.term };
|
||||
emails = [ e ];
|
||||
results.push(e);
|
||||
|
||||
@ -0,0 +1,199 @@
|
||||
import { wantsNewWindow } from 'discourse/lib/intercept-click';
|
||||
import afterTransition from 'discourse/lib/after-transition';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
elementId: null, //click detection added for data-{elementId}
|
||||
triggeringLinkClass: null, //the <a> classname where this card should appear
|
||||
_showCallback: null, //username, $target - load up data for when show is called, should call this._positionCard($target) when it's done.
|
||||
|
||||
postStream: Ember.computed.alias('topic.postStream'),
|
||||
viewingTopic: Ember.computed.match('currentPath', /^topic\./),
|
||||
|
||||
visible: false,
|
||||
username: null,
|
||||
loading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
isFixed: false,
|
||||
|
||||
_show(username, $target) {
|
||||
// No user card for anon
|
||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
username = Ember.Handlebars.Utils.escapeExpression(username.toString());
|
||||
|
||||
// Don't show on mobile
|
||||
if (this.site.mobileView) {
|
||||
DiscourseURL.routeTo(userPath(username));
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUsername = this.get('username');
|
||||
if (username === currentUsername && this.get('loading') === username) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postId = $target.parents('article').data('post-id');
|
||||
|
||||
const wasVisible = this.get('visible');
|
||||
const previousTarget = this.get('cardTarget');
|
||||
const target = $target[0];
|
||||
if (wasVisible) {
|
||||
this._close();
|
||||
if (target === previousTarget) { return; }
|
||||
}
|
||||
|
||||
const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
|
||||
this.setProperties({ username, loading: username, cardTarget: target, post });
|
||||
|
||||
this._showCallback(username, $target);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
afterTransition(this.$(), this._hide.bind(this));
|
||||
const id = this.get('elementId');
|
||||
const triggeringLinkClass = this.get('triggeringLinkClass');
|
||||
const clickOutsideEventName = `mousedown.outside-${id}`;
|
||||
const clickDataExpand = `click.discourse-${id}`;
|
||||
const clickMention = `click.discourse-${id}-${triggeringLinkClass}`;
|
||||
const previewClickEvent = `click.discourse-preview-${id}-${triggeringLinkClass}`;
|
||||
|
||||
this.setProperties({ clickOutsideEventName, clickDataExpand, clickMention, previewClickEvent });
|
||||
|
||||
$('html').off(clickOutsideEventName)
|
||||
.on(clickOutsideEventName, (e) => {
|
||||
if (this.get('visible')) {
|
||||
const $target = $(e.target);
|
||||
if ($target.closest(`[data-${id}]`).data(id) ||
|
||||
$target.closest(`a.${triggeringLinkClass}`).length > 0 ||
|
||||
$target.closest(`#${id}`).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._close();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$('#main-outlet').on(clickDataExpand, `[data-${id}]`, (e) => {
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
const $target = $(e.currentTarget);
|
||||
return this._show($target.data(id), $target);
|
||||
});
|
||||
|
||||
$('#main-outlet').on(clickMention, `a.${triggeringLinkClass}`, (e) => {
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
const $target = $(e.target);
|
||||
return this._show($target.text().replace(/^@/, ''), $target);
|
||||
});
|
||||
|
||||
this.appEvents.on(previewClickEvent, $target => {
|
||||
this.set('isFixed', true);
|
||||
return this._show($target.text().replace(/^@/, ''), $target);
|
||||
});
|
||||
},
|
||||
|
||||
_positionCard(target) {
|
||||
const rtl = ($('html').css('direction')) === 'rtl';
|
||||
if (!target) { return; }
|
||||
const width = this.$().width();
|
||||
const height = 175;
|
||||
const isFixed = this.get('isFixed');
|
||||
|
||||
let verticalAdjustments = 0;
|
||||
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
if (target) {
|
||||
let position = target.offset();
|
||||
if (position) {
|
||||
position.bottom = 'unset';
|
||||
|
||||
if (rtl) { // The site direction is rtl
|
||||
position.right = $(window).width() - position.left + 10;
|
||||
position.left = 'auto';
|
||||
let overage = ($(window).width() - 50) - (position.right + width);
|
||||
if (overage < 0) {
|
||||
position.right += overage;
|
||||
position.top += target.height() + 48;
|
||||
verticalAdjustments += target.height() + 48;
|
||||
}
|
||||
} else { // The site direction is ltr
|
||||
position.left += target.width() + 10;
|
||||
|
||||
let overage = ($(window).width() - 50) - (position.left + width);
|
||||
if (overage < 0) {
|
||||
position.left += overage;
|
||||
position.top += target.height() + 48;
|
||||
verticalAdjustments += target.height() + 48;
|
||||
}
|
||||
}
|
||||
|
||||
position.top -= $('#main-outlet').offset().top;
|
||||
if(isFixed) {
|
||||
position.top -= $('html').scrollTop();
|
||||
//if content is fixed and will be cut off on the bottom, display it above...
|
||||
if(position.top + height + verticalAdjustments > $(window).height() - 50) {
|
||||
position.bottom = $(window).height() - (target.offset().top - $('html').scrollTop());
|
||||
if(verticalAdjustments > 0) {
|
||||
position.bottom += 48;
|
||||
}
|
||||
position.top = 'unset';
|
||||
}
|
||||
}
|
||||
this.$().css(position);
|
||||
}
|
||||
|
||||
// After the card is shown, focus on the first link
|
||||
//
|
||||
// note: we DO NOT use afterRender here cause _positionCard may
|
||||
// run afterwards, if we allowed this to happen the usercard
|
||||
// may be offscreen and we may scroll all the way to it on focus
|
||||
Ember.run.next(null, () => this.$('a:first').focus() );
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_hide() {
|
||||
if (!this.get('visible')) {
|
||||
this.$().css({left: -9999, top: -9999});
|
||||
}
|
||||
},
|
||||
|
||||
_close() {
|
||||
this.setProperties({
|
||||
visible: false,
|
||||
username: null,
|
||||
loading: null,
|
||||
cardTarget: null,
|
||||
post: null,
|
||||
isFixed: false
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
const clickOutsideEventName = this.get('clickOutsideEventName');
|
||||
const clickDataExpand = this.get('clickDataExpand');
|
||||
const clickMention = this.get('clickMention');
|
||||
const previewClickEvent = this.get('previewClickEvent');
|
||||
$('html').off(clickOutsideEventName);
|
||||
$('#main').off(clickDataExpand).off(clickMention);
|
||||
this.appEvents.off(previewClickEvent);
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.keyCode === 27) { // ESC
|
||||
const target = this.get('cardTarget');
|
||||
this._close();
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -518,7 +518,8 @@ const Composer = RestModel.extend({
|
||||
targetUsernames: opts.usernames,
|
||||
composerTotalOpened: opts.composerTime,
|
||||
typingTime: opts.typingTime,
|
||||
whisper: opts.whisper
|
||||
whisper: opts.whisper,
|
||||
tags: opts.tags
|
||||
});
|
||||
|
||||
if (opts.post) {
|
||||
@ -836,7 +837,8 @@ const Composer = RestModel.extend({
|
||||
metaData: this.get('metaData'),
|
||||
usernames: this.get('targetUsernames'),
|
||||
composerTime: this.get('composerTime'),
|
||||
typingTime: this.get('typingTime')
|
||||
typingTime: this.get('typingTime'),
|
||||
tags: this.get('tags')
|
||||
};
|
||||
|
||||
this.set('draftStatus', I18n.t('composer.saving_draft_tip'));
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import PermissionType from 'discourse/models/permission-type';
|
||||
|
||||
const TagGroup = RestModel.extend({
|
||||
@computed('name', 'tag_names')
|
||||
@ -8,6 +9,31 @@ const TagGroup = RestModel.extend({
|
||||
return Ember.isEmpty(this.get('name')) || Ember.isEmpty(this.get('tag_names')) || this.get('saving');
|
||||
},
|
||||
|
||||
@computed('permissions')
|
||||
permissionName: {
|
||||
get(permissions) {
|
||||
if (!permissions) return 'public';
|
||||
|
||||
if (permissions['everyone'] === PermissionType.FULL) {
|
||||
return 'public';
|
||||
} else if (permissions['everyone'] === PermissionType.READONLY) {
|
||||
return 'visible';
|
||||
} else {
|
||||
return 'private';
|
||||
}
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if (value === 'private') {
|
||||
this.set('permissions', {'staff': PermissionType.FULL});
|
||||
} else if (value === 'visible') {
|
||||
this.set('permissions', {'staff': PermissionType.FULL, 'everyone': PermissionType.READONLY});
|
||||
} else {
|
||||
this.set('permissions', {'everyone': PermissionType.FULL});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
save() {
|
||||
let url = "/tag_groups";
|
||||
const self = this,
|
||||
@ -25,7 +51,7 @@ const TagGroup = RestModel.extend({
|
||||
tag_names: this.get('tag_names'),
|
||||
parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined,
|
||||
one_per_topic: this.get('one_per_topic'),
|
||||
permissions: this.get('visible_only_to_staff') ? {"staff": "1"} : {"everyone": "1"}
|
||||
permissions: this.get('permissions')
|
||||
},
|
||||
type: isNew ? 'POST' : 'PUT'
|
||||
}).then(function(result) {
|
||||
|
||||
@ -101,7 +101,7 @@ const Topic = RestModel.extend({
|
||||
const newTags = [];
|
||||
|
||||
tags.forEach(function(tag){
|
||||
if (title.toLowerCase().indexOf(tag) === -1 || Discourse.SiteSettings.staff_tags.indexOf(tag) !== -1) {
|
||||
if (title.toLowerCase().indexOf(tag) === -1) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
@ -14,7 +14,6 @@ export default function() {
|
||||
});
|
||||
|
||||
this.route('topicBySlugOrId', { path: '/t/:slugOrId', resetNamespace: true });
|
||||
this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' });
|
||||
|
||||
this.route('discovery', { path: '/', resetNamespace: true }, function() {
|
||||
// top
|
||||
|
||||
@ -118,8 +118,6 @@ export default (filterArg, params) => {
|
||||
this.controllerFor('discovery/topics').setProperties(topicOpts);
|
||||
this.searchService.set('searchContext', category.get('searchContext'));
|
||||
this.set('topics', null);
|
||||
|
||||
this.openTopicDraft(topics);
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
|
||||
@ -106,8 +106,6 @@ export default function(filter, extras) {
|
||||
}
|
||||
}
|
||||
this.controllerFor('discovery/topics').setProperties(topicOpts);
|
||||
|
||||
this.openTopicDraft(model);
|
||||
this.controllerFor('navigation/default').set('canCreateTopic', model.get('can_create_topic'));
|
||||
},
|
||||
|
||||
|
||||
@ -52,7 +52,25 @@ const DiscourseRoute = Ember.Route.extend({
|
||||
|
||||
refreshTitle() {
|
||||
Ember.run.once(this, this._refreshTitleOnce);
|
||||
},
|
||||
|
||||
clearTopicDraft() {
|
||||
// perhaps re-delegate this to root controller in all cases?
|
||||
// TODO also poison the store so it does not come back from the
|
||||
// dead
|
||||
if (this.get('controller.list.draft')) {
|
||||
this.set('controller.list.draft', null);
|
||||
}
|
||||
|
||||
if (this.controllerFor("discovery/categories").get('model.draft')) {
|
||||
this.controllerFor("discovery/categories").set('model.draft', null);
|
||||
}
|
||||
|
||||
if (this.controllerFor("discovery/topics").get('model.draft')) {
|
||||
this.controllerFor("discovery/topics").set('model.draft', null);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
redirectIfLoginRequired() {
|
||||
@ -63,17 +81,18 @@ const DiscourseRoute = Ember.Route.extend({
|
||||
},
|
||||
|
||||
openTopicDraft(model){
|
||||
// If there's a draft, open the create topic composer
|
||||
if (model.draft) {
|
||||
const composer = this.controllerFor('composer');
|
||||
if (!composer.get('model.viewOpen')) {
|
||||
composer.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draft: model.draft,
|
||||
draftKey: model.draft_key,
|
||||
draftSequence: model.draft_sequence
|
||||
});
|
||||
}
|
||||
const composer = this.controllerFor('composer');
|
||||
|
||||
if (composer.get('model.action') === Composer.CREATE_TOPIC &&
|
||||
composer.get('model.draftKey') === model.draft_key) {
|
||||
composer.set('model.composeState', Composer.OPEN);
|
||||
} else {
|
||||
composer.open({
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draft: model.draft,
|
||||
draftKey: model.draft_key,
|
||||
draftSequence: model.draft_sequence
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -89,8 +89,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
showCategoryAdmin: model.get("can_create_category"),
|
||||
canCreateTopic: model.get("can_create_topic"),
|
||||
});
|
||||
|
||||
this.openTopicDraft(model);
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -133,7 +131,12 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
},
|
||||
|
||||
createTopic() {
|
||||
this.openComposer(this.controllerFor("discovery/categories"));
|
||||
const model = this.controllerFor("discovery/categories").get('model');
|
||||
if (model.draft) {
|
||||
this.openTopicDraft(model);
|
||||
} else {
|
||||
this.openComposer(this.controllerFor("discovery/categories"));
|
||||
}
|
||||
},
|
||||
|
||||
didTransition() {
|
||||
|
||||
@ -45,7 +45,12 @@ export default Discourse.Route.extend(OpenComposer, {
|
||||
},
|
||||
|
||||
createTopic() {
|
||||
this.openComposer(this.controllerFor("discovery/topics"));
|
||||
const model = this.controllerFor("discovery/topics").get('model');
|
||||
if (model.draft) {
|
||||
this.openTopicDraft(model);
|
||||
} else {
|
||||
this.openComposer(this.controllerFor("discovery/topics"));
|
||||
}
|
||||
},
|
||||
|
||||
dismissReadTopics(dismissTopics) {
|
||||
|
||||
@ -129,18 +129,22 @@ export default Discourse.Route.extend({
|
||||
var controller = this.controllerFor("tags.show"),
|
||||
self = this;
|
||||
|
||||
this.controllerFor('composer').open({
|
||||
categoryId: controller.get('category.id'),
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: controller.get('list.draft_key'),
|
||||
draftSequence: controller.get('list.draft_sequence')
|
||||
}).then(function() {
|
||||
// Pre-fill the tags input field
|
||||
if (controller.get('model.id')) {
|
||||
var c = self.controllerFor('composer').get('model');
|
||||
c.set('tags', _.flatten([controller.get('model.id')], controller.get('additionalTags')));
|
||||
}
|
||||
});
|
||||
if (controller.get('list.draft')) {
|
||||
this.openTopicDraft(controller.get('list'));
|
||||
} else {
|
||||
this.controllerFor('composer').open({
|
||||
categoryId: controller.get('category.id'),
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: controller.get('list.draft_key'),
|
||||
draftSequence: controller.get('list.draft_sequence')
|
||||
}).then(function() {
|
||||
// Pre-fill the tags input field
|
||||
if (controller.get('model.id')) {
|
||||
var c = self.controllerFor('composer').get('model');
|
||||
c.set('tags', _.flatten([controller.get('model.id')], controller.get('additionalTags')));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
didTransition() {
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { loadTopicView } from 'discourse/models/topic';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model(params) {
|
||||
const topic = this.store.createRecord("topic", { id: params.id });
|
||||
return loadTopicView(topic).then(() => topic);
|
||||
},
|
||||
|
||||
afterModel(topic) {
|
||||
topic.set("details.notificationReasonText", null);
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.controllerFor("application").set("showFooter", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-badge'>
|
||||
<ul>
|
||||
{{#each options as |option|}}
|
||||
<li><a href>{{option.name}}</a></li>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-category'>
|
||||
<ul>
|
||||
{{#each options as |option|}}
|
||||
<li><a href>{{category-link option allowUncategorized="true" link="false"}}</a></li>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-category-or-tag'>
|
||||
<ul>
|
||||
{{#each options as |option|}}
|
||||
<li>
|
||||
|
||||
@ -5,5 +5,5 @@
|
||||
action=action
|
||||
icon="plus"
|
||||
disabled=disabled
|
||||
label="topic.create"}}
|
||||
label=label}}
|
||||
{{/if}}
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
{{create-topic-button
|
||||
canCreateTopic=canCreateTopic
|
||||
action=createTopic
|
||||
disabled=createTopicDisabled}}
|
||||
disabled=createTopicDisabled
|
||||
label=createTopicLabel
|
||||
}}
|
||||
|
||||
{{#if showCategoryEdit}}
|
||||
{{d-button
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
{{#if visible}}
|
||||
<div class="card-content">
|
||||
<div class="group-card-avatar">
|
||||
<a href={{groupPath}} {{action "showGroup"}} class="card-huge-avatar">
|
||||
{{avatar-flair
|
||||
flairURL=group.flair_url
|
||||
flairBgColor=group.flair_bg_color
|
||||
flairColor=group.flair_color
|
||||
groupName=group.name}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="names">
|
||||
<span>
|
||||
<h1 class="{{ group.name }}">
|
||||
<a href={{groupPath}} {{action "showGroup"}}>{{ group.name }}</a>
|
||||
</h1>
|
||||
{{#if group.full_name}}
|
||||
<h2 class='full-name'>{{group.full_name}}</h2>
|
||||
{{else}}
|
||||
<h2 class='username'>{{group.name}}</h2>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="usercard-controls group-details-button">
|
||||
{{group-membership-button
|
||||
model=group
|
||||
showLogin='showLogin'}}
|
||||
|
||||
{{#if group.messageable}}
|
||||
{{d-button
|
||||
action="messageGroup"
|
||||
class="btn-primary group-message-button inline"
|
||||
icon="envelope"
|
||||
label="groups.message"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
<h3><a href={{groupPath}} {{action "showGroup"}}>{{ group.user_count }} {{i18n 'groups.user_count'}}</a></h3>
|
||||
</div>
|
||||
<div class="members metadata">
|
||||
<span>
|
||||
{{#each group.members as |user|}}
|
||||
<a href={{user.path}} {{action "showUser" user}} class="card-tiny-avatar">{{bound-avatar user "tiny"}}</a>
|
||||
{{/each}}
|
||||
{{#if showMoreMembers}}
|
||||
<a href={{groupPath}} {{action "showGroup"}}>+{{ moreMembersCount }} {{i18n "more"}}</a>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -1,8 +1,7 @@
|
||||
{{#if visible}}
|
||||
<div class="card-content">
|
||||
|
||||
<div class="user-card-avatar">
|
||||
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
|
||||
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar user "huge"}}</a>
|
||||
{{#if user.primary_group_name}}
|
||||
{{avatar-flair
|
||||
flairURL=user.primary_group_flair_url
|
||||
@ -24,7 +23,7 @@
|
||||
{{#if user.name}}
|
||||
<h2 class='full-name'>{{user.name}}</h2>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{else}}
|
||||
<h2 class='username'>{{username}}</h2>
|
||||
{{/unless}}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-emoji'>
|
||||
<ul>
|
||||
{{#each options as |option|}}
|
||||
<li>
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
showBulkAddModal="showBulkAddModal"}}
|
||||
{{else}}
|
||||
{{d-button icon="plus"
|
||||
action="showAddMembersModal"
|
||||
label="groups.add_members.title"
|
||||
class="group-members-add"}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-group'>
|
||||
<ul>
|
||||
{{#each options as |option|}}
|
||||
<li><a href>{{option.name}}</a></li>
|
||||
|
||||
@ -4,18 +4,25 @@
|
||||
</td>
|
||||
{{/if}}
|
||||
|
||||
{{!--
|
||||
The `~` syntax strip spaces between the elements, making it produce
|
||||
`<a class=topic-post-badges>Some text</a><span class=topic-post-badges>`,
|
||||
with no space between them.
|
||||
This causes the topic-post-badge to be considered the same word as "text"
|
||||
at the end of the link, preventing it from line wrapping onto its own line.
|
||||
--}}
|
||||
<td class='main-link clearfix' colspan="{{titleColSpan}}">
|
||||
<span class='link-top-line'>
|
||||
{{raw-plugin-outlet name="topic-list-before-status"}}
|
||||
{{raw "topic-status" topic=topic}}
|
||||
{{topic-link topic class="raw-link raw-topic-link"}}
|
||||
{{#if topic.featured_link}}
|
||||
{{topic-featured-link topic}}
|
||||
{{/if}}
|
||||
{{raw-plugin-outlet name="topic-list-after-title"}}
|
||||
{{#if showTopicPostBadges}}
|
||||
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
|
||||
{{/if}}
|
||||
{{~raw-plugin-outlet name="topic-list-before-status"}}
|
||||
{{~raw "topic-status" topic=topic}}
|
||||
{{~topic-link topic class="raw-link raw-topic-link"}}
|
||||
{{~#if topic.featured_link}}
|
||||
{{~topic-featured-link topic}}
|
||||
{{~/if}}
|
||||
{{~raw-plugin-outlet name="topic-list-after-title"}}
|
||||
{{~#if showTopicPostBadges}}
|
||||
{{~raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
|
||||
{{~/if}}
|
||||
</span>
|
||||
|
||||
{{discourse-tags topic mode="list" tagsForUser=tagsForUser}}
|
||||
|
||||
@ -8,17 +8,17 @@
|
||||
<div>
|
||||
{{/unless~}}
|
||||
<div class='main-link'>
|
||||
{{raw "topic-status" topic=topic}}
|
||||
{{topic-link topic}}
|
||||
{{#if topic.featured_link}}
|
||||
{{topic-featured-link topic}}
|
||||
{{/if}}
|
||||
{{#if topic.unseen}}
|
||||
<span class="badge-notification new-topic"></span>
|
||||
{{/if}}
|
||||
{{~#if expandPinned}}
|
||||
{{raw "list/topic-excerpt" topic=topic}}
|
||||
{{/if~}}
|
||||
{{~raw "topic-status" topic=topic~}}
|
||||
{{~topic-link topic~}}
|
||||
{{~#if topic.featured_link~}}
|
||||
{{~topic-featured-link topic~}}
|
||||
{{~/if~}}
|
||||
{{~#if topic.unseen~}}
|
||||
<span class="badge-notification new-topic"></span>
|
||||
{{~/if~}}
|
||||
{{~#if expandPinned~}}
|
||||
{{~raw "list/topic-excerpt" topic=topic~}}
|
||||
{{~/if~}}
|
||||
</div>
|
||||
|
||||
<div class='pull-right'>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{{#load-more selector=".directory .user" action="loadMore"}}
|
||||
<div class="container">
|
||||
<div class='directory'>
|
||||
{{plugin-outlet name="users-top" connectorTagName='div' args=(hash model=model)}}
|
||||
|
||||
<div class='clearfix user-controls'>
|
||||
{{period-chooser period=period}}
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
<tr class="password-confirmation">
|
||||
<td><label for='new-account-password-confirmation'>{{i18n 'user.password_confirmation.title'}}</label></td>
|
||||
<td>
|
||||
{{input type="password" value=accountPasswordConfirm id="new-account-confirmation" autocomplete="false"}}
|
||||
{{input type="password" value=accountPasswordConfirm id="new-account-confirmation" autocomplete="new-password"}}
|
||||
{{input value=accountChallenge id="new-account-challenge"}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
class="input-xxlarge"
|
||||
usernames=model.usernames
|
||||
placeholderKey="groups.selector_placeholder"
|
||||
id="group-add-members-user-selector"}}
|
||||
id="group-add-members-user-selector"
|
||||
disallowEmails=true}}
|
||||
</div>
|
||||
|
||||
{{#if currentUser.admin}}
|
||||
|
||||
@ -5,5 +5,6 @@
|
||||
createCategory=(route-action "createCategory")
|
||||
reorderCategories=(route-action "reorderCategories")
|
||||
canCreateTopic=canCreateTopic
|
||||
hasDraft=draft
|
||||
createTopic=(route-action "createTopic")}}
|
||||
{{/d-section}}
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
canCreateTopic=canCreateTopic
|
||||
createTopic=(route-action "createTopic")
|
||||
createTopicDisabled=cannotCreateTopicOnCategory
|
||||
hasDraft=draft
|
||||
editCategory=(route-action "editCategory" category)}}
|
||||
|
||||
{{plugin-outlet name="category-navigation" args=(hash category=category)}}
|
||||
|
||||
@ -2,5 +2,6 @@
|
||||
{{d-navigation
|
||||
filterMode=filterMode
|
||||
canCreateTopic=canCreateTopic
|
||||
hasDraft=draft
|
||||
createTopic=(route-action "createTopic")}}
|
||||
{{/d-section}}
|
||||
|
||||
@ -29,10 +29,18 @@
|
||||
</section>
|
||||
|
||||
<section class="group-visibility">
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.visible_only_to_staff name="visible_only_to_staff"}}
|
||||
{{i18n 'tagging.groups.visible_only_to_staff'}}
|
||||
</label>
|
||||
<div>
|
||||
{{radio-button class="tag-permissions-choice" name="tag-permissions-choice" value="public" id="public-permission" selection=model.permissionName}}
|
||||
<label class="radio" for="public-permission">{{i18n 'tagging.groups.everyone_can_use'}}</label>
|
||||
</div>
|
||||
<div>
|
||||
{{radio-button class="tag-permissions-choice" name="tag-permissions-choice" value="visible" id="visible-permission" selection=model.permissionName}}
|
||||
<label class="radio" for="visible-permission">{{i18n 'tagging.groups.usable_only_by_staff'}}</label>
|
||||
</div>
|
||||
<div>
|
||||
{{radio-button class="tag-permissions-choice" name="tag-permissions-choice" value="private" id="private-permission" selection=model.permissionName}}
|
||||
<label class="radio" for="private-permission">{{i18n 'tagging.groups.visible_only_to_staff'}}</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'tagging.groups.save'}}</button>
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
{{create-topic-button
|
||||
canCreateTopic=canCreateTopic
|
||||
disabled=createTopicDisabled
|
||||
label=createTopicLabel
|
||||
action=(route-action "createTopic")}}
|
||||
|
||||
{{#if showTagFilter}}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<span class='topic-post-badges'>
|
||||
{{#if unread ~}}
|
||||
<a href='{{url}}' class='badge badge-notification unread' title='{{i18n "topic.unread_posts" count=unread}}'>{{unread}}</a>
|
||||
{{/if }}
|
||||
{{#if newPosts ~}}
|
||||
<a href='{{url}}' class='badge badge-notification new-posts' title='{{i18n "topic.total_unread_posts" count=newPosts}}'>{{newPosts}}</a>
|
||||
{{/if}}
|
||||
{{#if unseen ~}}
|
||||
<a href='{{url}}' class='badge badge-notification new-topic' title='{{i18n "topic.new"}}'>{{newDotText}}</a>
|
||||
{{/if}}
|
||||
{{~#if unread ~}}
|
||||
<a href='{{url}}' class='badge badge-notification unread' title='{{i18n "topic.unread_posts" count=unread}}'>{{unread}}</a>
|
||||
{{~/if}}
|
||||
{{~#if newPosts ~}}
|
||||
<a href='{{url}}' class='badge badge-notification new-posts' title='{{i18n "topic.total_unread_posts" count=newPosts}}'>{{newPosts}}</a>
|
||||
{{~/if}}
|
||||
{{~#if unseen ~}}
|
||||
<a href='{{url}}' class='badge badge-notification new-topic' title='{{i18n "topic.new"}}'>{{newDotText}}</a>
|
||||
{{~/if}}
|
||||
</span>
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
<div class='topic-unsubscribe'>
|
||||
<div class="container">
|
||||
<p>
|
||||
{{{stopNotificiationsText}}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{i18n "topic.unsubscribe.change_notification_state"}}
|
||||
</p>
|
||||
|
||||
{{topic-notifications-button notificationLevel=model.details.notification_level topic=model}}
|
||||
</div>
|
||||
</div>
|
||||
@ -4,4 +4,14 @@
|
||||
showUser="showUser"
|
||||
togglePosts="togglePosts"
|
||||
composePrivateMessage="composePrivateMessage"
|
||||
createNewMessageViaParams="createNewMessageViaParams"
|
||||
deleteUser="deleteUser"}}
|
||||
|
||||
{{group-card-contents
|
||||
currentPath=application.currentPath
|
||||
topic=topic.model
|
||||
showUser="showUser"
|
||||
togglePosts="togglePosts"
|
||||
composePrivateMessage="composePrivateMessage"
|
||||
createNewMessageViaParams="createNewMessageViaParams"
|
||||
deleteUser="deleteUser"}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class='autocomplete'>
|
||||
<div class='autocomplete ac-user'>
|
||||
<ul>
|
||||
{{#each options.users as |user|}}
|
||||
<li>
|
||||
|
||||
@ -86,6 +86,9 @@
|
||||
<div class="primary-textual">
|
||||
<h1 class="{{if nameFirst "full-name" "username"}}">{{if nameFirst model.name (format-username model.username)}} {{user-status model currentUser=currentUser}}</h1>
|
||||
<h2 class="{{if nameFirst "username" "full-name"}}">{{#if nameFirst}}{{model.username}}{{else}}{{model.name}}{{/if}}</h2>
|
||||
{{#if model.staged}}
|
||||
<h2 class="staged">{{i18n 'user.staged'}}</h2>
|
||||
{{/if}}
|
||||
{{#if model.title}}
|
||||
<h3>{{model.title}}</h3>
|
||||
{{/if}}
|
||||
@ -191,7 +194,7 @@
|
||||
</dd>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if canDeleteUser}}
|
||||
<div>{{d-button action="adminDelete" icon="exclamation-triangle" label="user.admin_delete" class="btn-danger"}}</div>
|
||||
{{/if}}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{{#load-more selector=".directory tbody tr" action="loadMore"}}
|
||||
<div class="container">
|
||||
<div class='directory'>
|
||||
|
||||
{{plugin-outlet name="users-top" connectorTagName='div' args=(hash model=model)}}
|
||||
<div class='clearfix'>
|
||||
{{period-chooser period=period}}
|
||||
{{text-field value=nameInput placeholderKey="directory.filter_name" class="filter-name no-blur"}}
|
||||
|
||||
@ -153,7 +153,7 @@ export default createWidget('hamburger-menu', {
|
||||
|
||||
const { site } = this;
|
||||
if (!site.mobileView && !this.capabilities.touch) {
|
||||
links.push({ action: 'showKeyboard', className: 'keyboard-shortcuts-link', label: 'keyboard_shortcuts_help.title' });
|
||||
links.push({ href: '', action: 'showKeyboard', className: 'keyboard-shortcuts-link', label: 'keyboard_shortcuts_help.title' });
|
||||
}
|
||||
|
||||
if (this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch)) {
|
||||
|
||||
@ -138,6 +138,7 @@ createWidget('header-icons', {
|
||||
iconId: 'toggle-hamburger-menu',
|
||||
active: attrs.hamburgerVisible,
|
||||
action: 'toggleHamburger',
|
||||
href: '',
|
||||
contents() {
|
||||
if (!attrs.flagCount) { return; }
|
||||
return h('div.badge-notification.flagged-posts', { attributes: {
|
||||
|
||||
@ -12,7 +12,7 @@ function animateHeart($elem, start, end, complete) {
|
||||
.animate({ textIndent: end }, {
|
||||
complete,
|
||||
step(now) {
|
||||
$(this).css('transform','scale('+now+')');
|
||||
$(this).css('transform','scale('+now+')').addClass("d-liked").removeClass("d-unliked");
|
||||
},
|
||||
duration: 150
|
||||
}, 'linear');
|
||||
|
||||
@ -14,7 +14,7 @@ createWidget('poster-name-title', {
|
||||
let titleContents = attrs.title;
|
||||
if (attrs.primaryGroupName) {
|
||||
const href = Discourse.getURL(`/groups/${attrs.primaryGroupName}`);
|
||||
titleContents = h('a.user-group', { className: attrs.extraClasses, attributes: { href } }, attrs.title);
|
||||
titleContents = h('a.user-group', { className: attrs.extraClasses, attributes: { href, 'data-group-card': attrs.primaryGroupName } }, attrs.title);
|
||||
}
|
||||
return titleContents;
|
||||
}
|
||||
|
||||
@ -99,6 +99,7 @@ createWidget('user-menu-dismiss-link', {
|
||||
attrs=(hash
|
||||
action="dismissNotifications"
|
||||
className="dismiss"
|
||||
tabindex="0"
|
||||
icon="check"
|
||||
label="user.dismiss"
|
||||
title="user.dismiss_notifications_tooltip")}}
|
||||
@ -144,6 +145,7 @@ export default createWidget('user-menu', {
|
||||
action: 'logout',
|
||||
className: 'logout',
|
||||
icon: 'sign-out',
|
||||
href: '',
|
||||
label: 'user.log_out'
|
||||
})
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@ import ComboBox from "select-kit/components/combo-box";
|
||||
import Tags from "select-kit/mixins/tags";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import renderTag from "discourse/lib/render-tag";
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
const { get, isEmpty, run, makeArray } = Ember;
|
||||
|
||||
export default ComboBox.extend(Tags, {
|
||||
@ -110,6 +111,7 @@ export default ComboBox.extend(Tags, {
|
||||
}
|
||||
|
||||
tags.map((tag) => {
|
||||
tag = escapeExpression(tag);
|
||||
const isHighlighted = highlightedSelection.map(s => get(s, "value")).includes(tag);
|
||||
output += `
|
||||
<button aria-label="${tag}" title="${tag}" class="selected-tag ${isHighlighted ? 'is-highlighted' : ''}" data-value="${tag}">
|
||||
|
||||
@ -80,7 +80,11 @@ self.addEventListener('fetch', function(event) {
|
||||
// If fetch() returns a valid HTTP response with an response code in the 4xx or 5xx
|
||||
// range, the catch() will NOT be called. If you need custom handling for 4xx or 5xx
|
||||
// errors, see https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/fallback-response
|
||||
return caches.match(OFFLINE_URL);
|
||||
if (!navigator.onLine) {
|
||||
return caches.match(OFFLINE_URL);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
@import "common/admin/customize";
|
||||
@import "common/admin/flagging";
|
||||
@import "common/admin/dashboard_next";
|
||||
@import "common/admin/moderation_history";
|
||||
@import "common/admin/suspend";
|
||||
|
||||
|
||||
177
app/assets/stylesheets/common/admin/dashboard_next.scss
Normal file
177
app/assets/stylesheets/common/admin/dashboard_next.scss
Normal file
@ -0,0 +1,177 @@
|
||||
.dashboard-next {
|
||||
|
||||
&.admin-contents {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.section-column {
|
||||
min-width: calc(50% - .5em);
|
||||
}
|
||||
|
||||
.section-column:last-child {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.section-column:first-child {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
.section-title {
|
||||
h2 {
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $primary-low-mid;
|
||||
margin-bottom: .5em;
|
||||
padding-bottom: .5em;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
padding: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.is-loading {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: .5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid $primary-low-mid;
|
||||
table-layout: fixed;
|
||||
|
||||
thead {
|
||||
tr {
|
||||
background: $primary-low;
|
||||
th {
|
||||
border: 1px solid $primary-low-mid;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
border: 1px solid $primary-low-mid;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.dashboard-mini-chart {
|
||||
width: calc(100% * (1/3));
|
||||
margin-bottom: 1em;
|
||||
flex-grow: 1;
|
||||
|
||||
&.is-loading {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.d-icon-question-circle {
|
||||
cursor: pointer;
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
h3 {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.double-up, &.up {
|
||||
.chart-trend, .data-point {
|
||||
color: rgb(17, 141, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&.double-down, &.down {
|
||||
.chart-trend, .data-point {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.one-data-point {
|
||||
.chart-container {
|
||||
min-height: 150px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.data-point {
|
||||
width: 100%;
|
||||
font-size: 6em;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
background: rgba(200,220,240,0.3);
|
||||
text-align: center;
|
||||
padding: .5em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.chart-trend {
|
||||
font-size: $font-up-5;
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
top: .5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.misc {
|
||||
.durability {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.durability-title {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -297,3 +297,16 @@ div.education {
|
||||
@extend .list-cell;
|
||||
border-bottom: 2px solid $primary-low;
|
||||
}
|
||||
|
||||
// This is not what we want:
|
||||
//
|
||||
// This is an overly-long topic title that would break just right
|
||||
// *
|
||||
//
|
||||
// Instead, we want the line to break like this:
|
||||
//
|
||||
// This is an overly-long topic title that would break just
|
||||
// right *
|
||||
.topic-post-badges {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -186,7 +186,7 @@ input {
|
||||
&[type="color"] {
|
||||
@include appearance-none;
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
padding: $input-padding;
|
||||
margin-bottom: 9px;
|
||||
font-size: $font-0;
|
||||
line-height: $line-height-large;
|
||||
|
||||
@ -86,11 +86,12 @@
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
transition: all linear .15s;
|
||||
outline: none;
|
||||
img.avatar {
|
||||
width: 2.2857em;
|
||||
height: 2.2857em;
|
||||
}
|
||||
&:hover {
|
||||
&:hover, &:focus {
|
||||
color: $primary;
|
||||
background-color: $primary-low;
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
@ -53,7 +53,7 @@ $iframe-ratio: 9/16 !default;
|
||||
|
||||
// Image-type options
|
||||
$include-image-type: true !default;
|
||||
$image-background: #444 !default;
|
||||
$image-background: linear-gradient(45deg, #111 0%,#333 100%) !default;
|
||||
$image-padding-top: 40px !default;
|
||||
$image-padding-bottom: 40px !default;
|
||||
$include-mobile-layout-for-image: true !default; // Removes paddings from top and bottom
|
||||
|
||||
@ -59,8 +59,9 @@
|
||||
a {
|
||||
padding: 0.25em 0.5em;
|
||||
display: block;
|
||||
&:hover {
|
||||
&:hover, &:focus {
|
||||
background-color: $highlight-medium;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,9 +244,19 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
span { color: $primary; }
|
||||
&:hover { background-color: $highlight-medium; }
|
||||
a { padding: 0; }
|
||||
span {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: $highlight-medium;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
@ -318,9 +329,18 @@ div.menu-links-header {
|
||||
border-spacing: 0 0.5em;
|
||||
.menu-links-row {
|
||||
display: table-row;
|
||||
li.glyphs {
|
||||
text-align: right;
|
||||
a {
|
||||
display: inline-flex;
|
||||
min-width: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
a:hover {
|
||||
a:hover, a:focus {
|
||||
background-color: $highlight-medium;
|
||||
outline: none;
|
||||
}
|
||||
a {
|
||||
padding: 0.5em;
|
||||
|
||||
@ -520,9 +520,7 @@ aside.onebox.stackexchange .onebox-body {
|
||||
|
||||
.onebox.githubcommit {
|
||||
pre.message {
|
||||
clear: left;
|
||||
padding: 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -237,6 +237,9 @@ header .discourse-tag {color: $tag-color }
|
||||
ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.tag-group-content {
|
||||
width: 75%;
|
||||
@ -249,11 +252,13 @@ header .discourse-tag {color: $tag-color }
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.group-tags-list .tag-chooser {
|
||||
width: 100%;
|
||||
}
|
||||
.btn {margin-left: 10px;}
|
||||
.saving {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
big { font-size: 2rem; }
|
||||
small { font-size: 0.75rem; }
|
||||
small small { font-size: .75em; }
|
||||
big big { font-size: 1em; }
|
||||
big big { font-size: 1em; }
|
||||
sub sub sub {bottom: 0;}
|
||||
sup sup sup {top: 0;}
|
||||
}
|
||||
@ -110,7 +110,7 @@
|
||||
.clearfix > .topic-meta-data > .names {
|
||||
span.user-title {
|
||||
background-color: dark-light-choose($highlight-low, $highlight-medium);
|
||||
color: dark-light-choose($primary-high, $secondary-low);
|
||||
color: dark-light-choose($primary-high, $secondary-low);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
@ -168,7 +168,6 @@ aside.quote {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
|
||||
.quote-controls {
|
||||
float: right;
|
||||
display: flex;
|
||||
@ -204,7 +203,7 @@ aside.quote {
|
||||
background: blend-primary-secondary(5%);
|
||||
border: 1px solid $primary-low;
|
||||
border-top: none; // would cause double top border
|
||||
|
||||
|
||||
.avatars {
|
||||
> div {
|
||||
float: left;
|
||||
@ -404,7 +403,7 @@ kbd
|
||||
background: dark-light-choose(#fafafa, #333);
|
||||
border: 1px solid dark-light-choose(#ccc, #555);
|
||||
border-bottom: medium none dark-light-choose(#fff, #000);
|
||||
|
||||
|
||||
color: $primary;
|
||||
display: inline-block;
|
||||
font-size: $font-down-1;
|
||||
@ -469,7 +468,7 @@ blockquote > *:last-child {
|
||||
max-width: 755px;
|
||||
border-top: 1px solid $primary-low;
|
||||
.topic-avatar {
|
||||
align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
padding: .7em 0;
|
||||
border-top: none;
|
||||
margin-right: 11px;
|
||||
@ -490,7 +489,7 @@ blockquote > *:last-child {
|
||||
}
|
||||
|
||||
.small-action-desc {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 100%;
|
||||
align-items: center;
|
||||
@ -501,7 +500,7 @@ blockquote > *:last-child {
|
||||
color: $primary-medium;
|
||||
|
||||
.custom-message {
|
||||
flex: 1 1 100%;
|
||||
flex: 1 1 100%;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
font-size: $font-up-1;
|
||||
|
||||
@ -192,20 +192,6 @@ a.badge-category {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topic-unsubscribe {
|
||||
.notifications-button {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
line-height: $line-height-large;
|
||||
.dropdown-toggle {
|
||||
float: none;
|
||||
}
|
||||
.dropdown-menu {
|
||||
bottom: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-links-container {
|
||||
@include unselectable;
|
||||
clear: both;
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
margin-right: 15px;
|
||||
margin: 4px 0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
|
||||
dt {
|
||||
color: $secondary-medium;
|
||||
margin-right: 5px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@ -592,3 +592,8 @@
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-textual .staged,
|
||||
#user-card .staged {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@ -72,3 +72,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ $small-width: 800px !default;
|
||||
$medium-width: 995px !default;
|
||||
$large-width: 1110px !default;
|
||||
|
||||
$input-padding: 4px 10px;
|
||||
|
||||
// Brand color variables
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@ -3,10 +3,6 @@
|
||||
&.category-chooser {
|
||||
width: 300px;
|
||||
|
||||
.combo-box-header {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.select-kit-row {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
|
||||
@ -70,6 +70,7 @@
|
||||
min-width: 100px;
|
||||
max-height: 300px;
|
||||
max-width: 30em;
|
||||
padding: 4px 0;
|
||||
|
||||
.collection-header {
|
||||
.category-filter {
|
||||
@ -79,7 +80,7 @@
|
||||
line-height: $line-height-medium;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
padding: 10px 5px;
|
||||
padding: 6px 10px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@ -101,7 +102,7 @@
|
||||
font-weight: bold;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 5px;
|
||||
padding: 5px 10px;
|
||||
|
||||
.category-desc {
|
||||
font-weight: normal;
|
||||
@ -117,7 +118,7 @@
|
||||
}
|
||||
|
||||
&:not(.no-content) {
|
||||
padding: 5px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.topic-count {
|
||||
|
||||
@ -5,14 +5,14 @@
|
||||
}
|
||||
|
||||
.select-kit-row {
|
||||
margin: 4px;
|
||||
margin: 0;
|
||||
min-height: 1px;
|
||||
padding: 4px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.select-kit-filter {
|
||||
line-height: $line-height-medium;
|
||||
padding: 5px 6px;
|
||||
padding: $input-padding;
|
||||
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
.select-kit-header {
|
||||
background: $secondary;
|
||||
border: 1px solid $primary-medium;
|
||||
padding: 4px;
|
||||
padding: $input-padding;
|
||||
font-weight: 500;
|
||||
font-size: $font-0;
|
||||
line-height: $line-height-large;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user