Version bump

This commit is contained in:
Neil Lalonde 2018-04-24 11:17:30 -04:00
commit 7e69341dcd
327 changed files with 4263 additions and 1437 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
activate() {
this.controllerFor('admin-dashboard-next').fetchDashboard();
}
});

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1 +1,4 @@
export default Ember.Component.extend({ tagName: '' });
export default Ember.Component.extend({
tagName: '',
label: 'topic.create'
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<div class='autocomplete'>
<div class='autocomplete ac-badge'>
<ul>
{{#each options as |option|}}
<li><a href>{{option.name}}</a></li>

View File

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

View File

@ -1,4 +1,4 @@
<div class='autocomplete'>
<div class='autocomplete ac-category-or-tag'>
<ul>
{{#each options as |option|}}
<li>

View File

@ -5,5 +5,5 @@
action=action
icon="plus"
disabled=disabled
label="topic.create"}}
label=label}}
{{/if}}

View File

@ -15,7 +15,9 @@
{{create-topic-button
canCreateTopic=canCreateTopic
action=createTopic
disabled=createTopicDisabled}}
disabled=createTopicDisabled
label=createTopicLabel
}}
{{#if showCategoryEdit}}
{{d-button

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<div class='autocomplete'>
<div class='autocomplete ac-emoji'>
<ul>
{{#each options as |option|}}
<li>

View File

@ -10,6 +10,7 @@
showBulkAddModal="showBulkAddModal"}}
{{else}}
{{d-button icon="plus"
action="showAddMembersModal"
label="groups.add_members.title"
class="group-members-add"}}
{{/if}}

View File

@ -1,4 +1,4 @@
<div class='autocomplete'>
<div class='autocomplete ac-group'>
<ul>
{{#each options as |option|}}
<li><a href>{{option.name}}</a></li>

View File

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

View File

@ -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~}}
&nbsp;<span class="badge-notification new-topic"></span>
{{~/if~}}
{{~#if expandPinned~}}
{{~raw "list/topic-excerpt" topic=topic~}}
{{~/if~}}
</div>
<div class='pull-right'>

View File

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

View File

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

View File

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

View File

@ -5,5 +5,6 @@
createCategory=(route-action "createCategory")
reorderCategories=(route-action "reorderCategories")
canCreateTopic=canCreateTopic
hasDraft=draft
createTopic=(route-action "createTopic")}}
{{/d-section}}

View File

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

View File

@ -2,5 +2,6 @@
{{d-navigation
filterMode=filterMode
canCreateTopic=canCreateTopic
hasDraft=draft
createTopic=(route-action "createTopic")}}
{{/d-section}}

View File

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

View File

@ -19,6 +19,7 @@
{{create-topic-button
canCreateTopic=canCreateTopic
disabled=createTopicDisabled
label=createTopicLabel
action=(route-action "createTopic")}}
{{#if showTagFilter}}

View File

@ -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 ~}}
&nbsp;<a href='{{url}}' class='badge badge-notification unread' title='{{i18n "topic.unread_posts" count=unread}}'>{{unread}}</a>
{{~/if}}
{{~#if newPosts ~}}
&nbsp;<a href='{{url}}' class='badge badge-notification new-posts' title='{{i18n "topic.total_unread_posts" count=newPosts}}'>{{newPosts}}</a>
{{~/if}}
{{~#if unseen ~}}
&nbsp;<a href='{{url}}' class='badge badge-notification new-topic' title='{{i18n "topic.new"}}'>{{newDotText}}</a>
{{~/if}}
</span>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<div class='autocomplete'>
<div class='autocomplete ac-user'>
<ul>
{{#each options.users as |user|}}
<li>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -520,9 +520,7 @@ aside.onebox.stackexchange .onebox-body {
.onebox.githubcommit {
pre.message {
clear: left;
padding: 0;
padding-top: 10px;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -72,3 +72,4 @@
}
}
}

View File

@ -9,6 +9,8 @@ $small-width: 800px !default;
$medium-width: 995px !default;
$large-width: 1110px !default;
$input-padding: 4px 10px;
// Brand color variables
// --------------------------------------------------

View File

@ -3,10 +3,6 @@
&.category-chooser {
width: 300px;
.combo-box-header {
padding: 4px;
}
.select-kit-row {
display: -webkit-box;
display: -ms-flexbox;

View File

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

View File

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