Version bump

This commit is contained in:
Neil Lalonde 2019-01-02 15:33:06 -05:00
commit 5771b29d19
369 changed files with 5321 additions and 2860 deletions

View File

@ -46,6 +46,7 @@ cache:
- vendor/bundle
before_install:
- wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- nvm install node
- node --version
- gem install bundler

View File

@ -36,7 +36,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.69'
gem 'onebox', '1.8.71'
gem 'http_accept_language', '~>2.0.5', require: false

View File

@ -131,7 +131,7 @@ GEM
exifr (1.3.4)
fabrication (2.20.1)
fakeweb (1.3.0)
faraday (0.12.2)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday-http-cache (1.3.1)
faraday (~> 0.8)
@ -152,7 +152,7 @@ GEM
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
hashdiff (0.3.7)
hashie (3.5.7)
hashie (3.6.0)
highline (1.7.10)
hiredis (0.6.1)
hkdf (0.3.0)
@ -168,7 +168,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jwt (1.5.6)
jwt (2.1.0)
kgio (2.11.2)
kramdown (1.17.0)
libv8 (6.7.288.46.1)
@ -184,7 +184,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (1.3.1)
logster (1.3.4)
loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -222,25 +222,25 @@ GEM
nokogumbo (1.5.0)
nokogiri
oauth (0.5.4)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
oauth2 (1.4.1)
faraday (>= 0.8, < 0.16.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
oj (3.6.2)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3)
omniauth-facebook (5.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.3.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-google-oauth2 (0.5.3)
jwt (>= 1.5)
omniauth-google-oauth2 (0.6.0)
jwt (>= 2.0)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
omniauth-instagram (1.3.0)
@ -249,16 +249,16 @@ GEM
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
omniauth-oauth2 (1.5.0)
omniauth-oauth2 (1.6.0)
oauth2 (~> 1.1)
omniauth (~> 1.2)
omniauth (~> 1.9)
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.69)
onebox (1.8.71)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@ -357,7 +357,7 @@ GEM
rspec-support (~> 3.7.0)
rspec-support (3.7.1)
rtlit (0.0.5)
rubocop (0.60.0)
rubocop (0.61.1)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1)
@ -426,7 +426,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.4.0)
unicode-display_width (1.4.1)
unicorn (5.4.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@ -512,7 +512,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.69)
onebox (= 1.8.71)
openid-redis-store
pg
pry-nav
@ -557,4 +557,4 @@ DEPENDENCIES
webpush
BUNDLED WITH
1.17.1
1.17.2

View File

@ -0,0 +1,23 @@
export default Ember.Component.extend({
tagName: "",
buffer: "",
editing: false,
init() {
this._super(...arguments);
this.set("editing", false);
},
actions: {
edit() {
this.set("buffer", this.get("value"));
this.toggleProperty("editing");
},
save() {
// Action has to toggle 'editing' property.
this.action(this.get("buffer"));
}
}
});

View File

@ -0,0 +1,40 @@
import { setting } from "discourse/lib/computed";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
classNames: ["admin-report-storage-stats"],
backupLocation: setting("backup_location"),
backupStats: Ember.computed.alias("model.data.backups"),
uploadStats: Ember.computed.alias("model.data.uploads"),
@computed("backupStats")
showBackupStats(stats) {
return stats && this.currentUser.admin;
},
@computed("backupLocation")
backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`);
},
@computed("backupStats.used_bytes")
usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@computed("backupStats.free_bytes")
freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@computed("uploadStats.used_bytes")
usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@computed("uploadStats.free_bytes")
freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
}
});

View File

@ -106,7 +106,12 @@ export default Ember.Component.extend({
pages(data, perPage, page) {
if (!data || data.length <= perPage) return [];
let pages = [...Array(Math.ceil(data.length / perPage)).keys()].map(v => {
const pagesIndexes = [];
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
pagesIndexes.push(i);
}
let pages = pagesIndexes.map(v => {
return {
page: v + 1,
index: v,

View File

@ -16,24 +16,12 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
isLoading: false,
dashboardFetchedAt: null,
exceptionController: Ember.inject.controller("exception"),
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
logSearchQueriesEnabled: setting("log_search_queries"),
lastBackupTakenAt: Ember.computed.alias(
"model.attributes.last_backup_taken_at"
),
shouldDisplayDurability: Ember.computed.and("diskSpace"),
basePath: Discourse.BaseUri,
@computed
activityMetrics() {
return [
"page_view_total_reqs",
"visits",
"time_to_first_response",
"likes",
"flags",
"user_to_user_private_messages_with_replies"
];
@computed("siteSettings.dashboard_general_tab_activity_metrics")
activityMetrics(metrics) {
return (metrics || "").split("|").filter(m => m);
},
@computed
@ -65,7 +53,7 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
trendingSearchFilters() {
return {
startDate: moment()
.subtract(6, "days")
.subtract(1, "month")
.startOf("day"),
endDate: this.get("today")
};
@ -87,6 +75,7 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
usersByTypeReport: staticReport("users_by_type"),
usersByTrustLevelReport: staticReport("users_by_trust_level"),
storageReport: staticReport("storage_report"),
fetchDashboard() {
if (this.get("isLoading")) return;
@ -129,13 +118,6 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
.format("LLL");
},
@computed("lastBackupTakenAt")
backupTimestamp(lastBackupTakenAt) {
return moment(lastBackupTakenAt)
.tz(moment.tz.guess())
.format("LLL");
},
_reportsForPeriodURL(period) {
return Discourse.getURL(`/admin?period=${period}`);
}

View File

@ -0,0 +1,30 @@
import computed from "ember-addons/ember-computed-decorators";
const { get } = Ember;
export default Ember.Controller.extend({
filter: null,
@computed("model.[]", "filter")
filterReports(reports, filter) {
if (filter) {
filter = filter.toLowerCase();
return reports.filter(report => {
return (
(get(report, "title") || "").toLowerCase().indexOf(filter) > -1 ||
(get(report, "description") || "").toLowerCase().indexOf(filter) > -1
);
});
}
return reports;
},
actions: {
filterReports(filter) {
Ember.run.debounce(this, this._performFiltering, filter, 250);
}
},
_performFiltering(filter) {
this.set("filter", filter);
}
});

View File

@ -4,7 +4,6 @@ import AdminUser from "admin/models/admin-user";
import computed from "ember-addons/ember-computed-decorators";
const ATTRIBUTES = [
"disk_space",
"admins",
"moderators",
"silenced",

View File

@ -1,4 +1,5 @@
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Ember.Controller.extend(GrantBadgeController, {
adminUser: Ember.inject.controller(),
@ -70,9 +71,8 @@ export default Ember.Controller.extend(GrantBadgeController, {
}
});
},
function() {
// Failure
bootbox.alert(I18n.t("generic_error"));
function(error) {
popupAjaxError(error);
}
);
},

View File

@ -7,9 +7,6 @@ import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(CanCheckEmails, {
adminTools: Ember.inject.service(),
editingUsername: false,
editingName: false,
editingTitle: false,
originalPrimaryGroupId: null,
customGroupIdsBuffer: null,
availableGroups: null,
@ -244,17 +241,12 @@ export default Ember.Controller.extend(CanCheckEmails, {
this.get("adminTools").showSilenceModal(this.get("model"));
},
toggleUsernameEdit() {
this.set("userUsernameValue", this.get("model.username"));
this.toggleProperty("editingUsername");
},
saveUsername() {
saveUsername(newUsername) {
const oldUsername = this.get("model.username");
this.set("model.username", this.get("userUsernameValue"));
this.set("model.username", newUsername);
return ajax(`/users/${oldUsername.toLowerCase()}/preferences/username`, {
data: { new_username: this.get("userUsernameValue") },
data: { new_username: newUsername },
type: "PUT"
})
.catch(e => {
@ -264,19 +256,14 @@ export default Ember.Controller.extend(CanCheckEmails, {
.finally(() => this.toggleProperty("editingUsername"));
},
toggleNameEdit() {
this.set("userNameValue", this.get("model.name"));
this.toggleProperty("editingName");
},
saveName() {
saveName(newName) {
const oldName = this.get("model.name");
this.set("model.name", this.get("userNameValue"));
this.set("model.name", newName);
return ajax(
userPath(`${this.get("model.username").toLowerCase()}.json`),
{
data: { name: this.get("userNameValue") },
data: { name: newName },
type: "PUT"
}
)
@ -287,24 +274,19 @@ export default Ember.Controller.extend(CanCheckEmails, {
.finally(() => this.toggleProperty("editingName"));
},
toggleTitleEdit() {
this.set("userTitleValue", this.get("model.title"));
this.toggleProperty("editingTitle");
},
saveTitle(newTitle) {
const oldTitle = this.get("model.title");
saveTitle() {
const prevTitle = this.get("userTitleValue");
this.set("model.title", this.get("userTitleValue"));
this.set("model.title", newTitle);
return ajax(
userPath(`${this.get("model.username").toLowerCase()}.json`),
{
data: { title: this.get("userTitleValue") },
data: { title: newTitle },
type: "PUT"
}
)
.catch(e => {
this.set("model.title", prevTitle);
this.set("model.title", oldTitle);
popupAjaxError(e);
})
.finally(() => this.toggleProperty("editingTitle"));

View File

@ -2,8 +2,9 @@ import debounce from "discourse/lib/debounce";
import { i18n } from "discourse/lib/computed";
import AdminUser from "admin/models/admin-user";
import { observes } from "ember-addons/ember-computed-decorators";
import CanCheckEmails from "discourse/mixins/can-check-emails";
export default Ember.Controller.extend({
export default Ember.Controller.extend(CanCheckEmails, {
query: null,
queryParams: ["order", "ascending"],
order: null,

View File

@ -17,6 +17,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
checkPrivate: Ember.computed.match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
urlPlaceholder: "https://github.com/discourse/sample_theme",
@computed("loading", "remote", "uploadUrl", "local", "localFile")
importDisabled(isLoading, isRemote, uploadUrl, isLocal, localFile) {
@ -25,6 +26,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
@observes("privateChecked")
privateWasChecked() {
this.get("privateChecked")
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
const checked = this.get("privateChecked");
if (checked && !this._keyLoading) {
this._keyLoading = true;

View File

@ -1,6 +1,6 @@
import { ajax } from "discourse/lib/ajax";
const GENERAL_ATTRIBUTES = ["disk_space", "updated_at", "last_backup_taken_at"];
const GENERAL_ATTRIBUTES = ["updated_at"];
const AdminDashboardNext = Discourse.Model.extend({});

View File

@ -272,6 +272,7 @@ const Report = Discourse.Model.extend({
if (type === "seconds") return this._secondsLabel(value);
if (type === "link") return this._linkLabel(label.properties, row);
if (type === "percent") return this._percentLabel(value);
if (type === "bytes") return this._bytesLabel(value);
if (type === "number") {
return this._numberLabel(value, opts);
}
@ -381,6 +382,13 @@ const Report = Discourse.Model.extend({
};
},
_bytesLabel(value) {
return {
value,
formatedValue: I18n.toHumanSize(value)
};
},
_dateLabel(value, date, format = "LL") {
return {
value,

View File

@ -0,0 +1,11 @@
import { ajax } from "discourse/lib/ajax";
export default Discourse.Route.extend({
model() {
return ajax("/admin/reports").then(json => json);
},
setupController(controller, model) {
controller.setProperties({ model: model.reports, filter: null });
}
});

View File

@ -1,13 +1,5 @@
import { ajax } from "discourse/lib/ajax";
export default Discourse.Route.extend({
model() {
return ajax("/admin/reports").then(json => {
return json;
});
},
setupController(controller, model) {
controller.setProperties({ model: model.reports });
beforeModel() {
this.transitionTo("admin.dashboardNextReports");
}
});

View File

@ -12,6 +12,10 @@ export default function() {
path: "/dashboard/security",
resetNamespace: true
});
this.route("admin.dashboardNextReports", {
path: "/dashboard/reports",
resetNamespace: true
});
});
this.route(

View File

@ -6,9 +6,9 @@
{{/if}}
{{#if site.isReadOnly}}
{{d-button class="btn-default" icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}}
{{d-button class="btn-default" icon="far-eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}}
{{else}}
{{d-button class="btn-default" icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}}
{{d-button class="btn-default" icon="far-eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}}
{{/if}}
</div>
<table class="grid">
@ -31,10 +31,10 @@
title="admin.backups.operations.download.title"
label="admin.backups.operations.download.label"}}
{{#if status.isOperationRunning}}
{{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" disabled="true" title="admin.backups.operations.is_running"}}
{{d-button icon="far-trash-alt" action="destroyBackup" actionParam=backup class="btn-danger" disabled="true" title="admin.backups.operations.is_running"}}
{{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled class="btn-default" title=restoreTitle label="admin.backups.operations.restore.label"}}
{{else}}
{{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" title="admin.backups.operations.destroy.title"}}
{{d-button icon="far-trash-alt" action="destroyBackup" actionParam=backup class="btn-danger" title="admin.backups.operations.destroy.title"}}
{{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled class="btn-default" title=restoreTitle label="admin.backups.operations.restore.label"}}
{{/if}}
</div>

View File

@ -0,0 +1,16 @@
<div class='field'>{{i18n name}}</div>
<div class='value'>
{{#if editing}}
{{text-field value=buffer autofocus="autofocus"}}
{{else}}
<span {{action "edit"}}>{{value}}</span>
{{/if}}
</div>
<div class='controls'>
{{#if editing}}
{{d-button class="btn-default" action=(action "save") label="admin.user_fields.save"}}
<a href {{action "edit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button class="btn-default" action=(action "edit") icon="pencil"}}
{{/if}}
</div>

View File

@ -0,0 +1,33 @@
{{#if showBackupStats}}
<div class="backups">
<h3 class="storage-stats-title">
<a href="{{get-url '/admin/backups'}}">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
</h3>
<p>
{{#if backupStats.free_bytes}}
{{i18n "admin.dashboard.space_used_and_free" usedSize=usedBackupSpace freeSize=freeBackupSpace}}
{{else}}
{{i18n "admin.dashboard.space_used" usedSize=usedBackupSpace}}
{{/if}}
<br>
{{i18n "admin.dashboard.backup_count" count=backupStats.count location=backupLocationName}}
{{#if backupStats.last_backup_taken_at}}
<br>
{{{i18n "admin.dashboard.lastest_backup" date=(format-date backupStats.last_backup_taken_at leaveAgo="true")}}}
{{/if}}
</p>
</div>
{{/if}}
<div class="uploads">
<h3 class="storage-stats-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
<p>
{{#if uploadStats.free_bytes}}
{{i18n "admin.dashboard.space_used_and_free" usedSize=usedUploadSpace freeSize=freeUploadSpace}}
{{else}}
{{i18n "admin.dashboard.space_used" usedSize=usedUploadSpace}}
{{/if}}
</p>
</div>

View File

@ -6,7 +6,7 @@
<ul class="breadcrumb">
{{#if showAllReportsLink}}
<li class="item all-reports">
{{#link-to "adminReports" class="report-url"}}
{{#link-to "admin.dashboardNextReports" class="report-url"}}
{{i18n "admin.dashboard.all_reports"}}
{{/link-to}}
</li>
@ -112,17 +112,15 @@
{{#if showFilteringUI}}
<div class="filters">
{{#if showModes}}
<ul class="modes">
<div class="modes">
{{#each displayedModes as |displayedMode|}}
<li class="mode">
{{d-button
action="changeMode"
actionParam=displayedMode.mode
class=displayedMode.cssClass
icon=displayedMode.icon}}
</li>
{{d-button
action="changeMode"
actionParam=displayedMode.mode
class=displayedMode.cssClass
icon=displayedMode.icon}}
{{/each}}
</ul>
</div>
{{/if}}
{{#if showDatesOptions}}

View File

@ -26,6 +26,11 @@
{{i18n "admin.dashboard.security_tab"}}
{{/link-to}}
</li>
<li class="navigation-item reports">
{{#link-to "admin.dashboardNextReports" class="navigation-link"}}
{{i18n "admin.dashboard.reports_tab"}}
{{/link-to}}
</li>
</ul>
{{outlet}}

View File

@ -5,7 +5,7 @@
<div class="period-section">
<div class="section-title">
<h2>
<a href="{{get-url '/admin/reports'}}">
<a href="{{get-url '/admin/dashboard/reports'}}">
{{i18n "admin.dashboard.community_health"}}
</a>
</h2>
@ -56,40 +56,38 @@
<div class="section-columns">
<div class="section-column">
<div class="admin-report activity-metrics">
<div class="header">
<ul class="breadcrumb">
<li class="item report">
{{#link-to "adminReports" class="report-url"}}
{{i18n "admin.dashboard.activity_metrics"}}
{{/link-to}}
</li>
</ul>
</div>
<div class="report-body">
<div class="counters-list">
<div class="counters-header">
<div class="counters-cell"></div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.today'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.yesterday'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.last_7_days'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.last_30_days'}}</div>
</div>
{{#if activityMetrics.length}}
<div class="admin-report activity-metrics">
<div class="header">
<ul class="breadcrumb">
<li class="item report">
{{#link-to "adminReports" class="report-url"}}
{{i18n "admin.dashboard.activity_metrics"}}
{{/link-to}}
</li>
</ul>
</div>
<div class="report-body">
<div class="counters-list">
<div class="counters-header">
<div class="counters-cell"></div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.today'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.yesterday'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.last_7_days'}}</div>
<div class="counters-cell">{{i18n 'admin.dashboard.reports.last_30_days'}}</div>
</div>
{{#each activityMetrics as |metric|}}
{{admin-report
showHeader=false
filters=activityMetricsFilters
forcedModes="counters"
dataSourceName=metric}}
{{/each}}
{{#each activityMetrics as |metric|}}
{{admin-report
showHeader=false
filters=activityMetricsFilters
forcedModes="counters"
dataSourceName=metric}}
{{/each}}
</div>
</div>
</div>
{{#link-to "adminReports"}}
{{i18n "admin.dashboard.all_reports"}}
{{/link-to}}
</div>
{{/if}}
<div class="user-metrics">
{{#conditional-loading-section isLoading=isLoading}}
@ -103,51 +101,26 @@
{{/conditional-loading-section}}
</div>
{{#conditional-loading-section isLoading=isLoading title=(i18n "admin.dashboard.backups")}}
<div class="misc">
<div class="misc">
{{admin-report
forcedModes="storage-stats"
dataSourceName="storage_stats"
showHeader=false}}
{{#if shouldDisplayDurability}}
<div class="durability">
{{#if currentUser.admin}}
<div class="backups">
<h3 class="durability-title">
<a href="{{get-url '/admin/backups'}}">{{d-icon "archive"}} {{i18n "admin.dashboard.backups"}}</a>
</h3>
<p>
{{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
{{#if lastBackupTakenAt}}
<br />
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
{{/if}}
</p>
</div>
{{/if}}
<div class="uploads">
<h3 class="durability-title">{{d-icon "upload"}} {{i18n "admin.dashboard.uploads"}}</h3>
<p>
{{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
</p>
</div>
</div>
{{/if}}
<div class="last-dashboard-update">
<div>
<div class="last-dashboard-update">
<div>
<h4>{{i18n "admin.dashboard.last_updated"}} </h4>
<p>{{updatedTimestamp}}</p>
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn btn-default">
{{i18n "admin.dashboard.whats_new_in_discourse"}}
</a>
</div>
<a rel="noopener" target="_blank" href="https://meta.discourse.org/tags/release-notes" class="btn btn-default">
{{i18n "admin.dashboard.whats_new_in_discourse"}}
</a>
</div>
</div>
</div>
<p>
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
</p>
{{/conditional-loading-section}}
<p>
{{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}}
</p>
</div>
<div class="section-column">

View File

@ -4,7 +4,7 @@
<div class="moderators-activity section">
<div class="section-title">
<h2>
<a href="{{get-url '/admin/reports/moderators_activity'}}">
<a href="{{get-url '/admin/dashboard/reports/moderators_activity'}}">
{{i18n "admin.dashboard.moderators_activity"}}
</a>
</h2>

View File

@ -0,0 +1,28 @@
{{#conditional-loading-spinner condition=isLoading}}
<div class="reports-index section">
<div class="section-title">
<h2>{{i18n "admin.reports.title"}}</h2>
{{input
class="filter-reports-input"
input=(action "filterReports" value="target.value")
placeholder=(i18n "admin.dashboard.filter_reports")
autofocus=true}}
</div>
<ul class="reports-list">
{{#each filterReports as |report|}}
<li class="report">
{{#link-to 'adminReports.show' report.type}}
<h3 class="report-title">{{report.title}}</h3>
{{#if report.description}}
<p class="report-description">
{{report.description}}
</p>
{{/if}}
{{/link-to}}
</li>
{{/each}}
</ul>
</div>
{{/conditional-loading-spinner}}

View File

@ -15,7 +15,7 @@
{{#if remote}}
<div class="inputs">
<div class='repo'>
{{input value=uploadUrl placeholder="https://github.com/discourse/sample_theme"}}
{{input value=uploadUrl placeholder=urlPlaceholder}}
<span class="description">{{i18n 'admin.customize.theme.import_web_tip'}}</span>
</div>
<div class='branch'>

View File

@ -1,17 +0,0 @@
<h3>{{i18n "admin.reports.title"}}</h3>
<ul class="reports-list">
{{#each model as |report|}}
<li class="report">
{{#link-to 'adminReports.show' report.type}}
<h4 class="report-title">{{report.title}}</h4>
{{/link-to}}
{{#if report.description}}
<p class="report-description">
{{report.description}}
</p>
{{/if}}
</li>
{{/each}}
</ul>

View File

@ -10,8 +10,7 @@
<thead>
<th class="col heading term">{{i18n 'admin.logs.search_logs.term'}}</th>
<th class="col heading">{{i18n 'admin.logs.search_logs.searches'}}</th>
<th class="col heading">{{i18n 'admin.logs.search_logs.click_through'}}</th>
<th class="col heading" title="{{i18n 'admin.logs.search_logs.unique_title'}}">{{i18n 'admin.logs.search_logs.unique'}}</th>
<th class="col heading">{{i18n 'admin.logs.search_logs.click_through_rate'}}</th>
</thead>
<tbody>
{{#each model as |item|}}
@ -20,8 +19,7 @@
{{#link-to 'adminSearchLogs.term' item.term}}{{item.term}}{{/link-to}}
</td>
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.searches'}}</div>{{item.searches}}</td>
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.click_through'}}</div>{{item.click_through}}</td>
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.unique'}}</div>{{item.unique_searches}}</td>
<td class="col"><div class="label">{{i18n 'admin.logs.search_logs.click_through_rate'}}</div>{{item.ctr}}%</td>
</tr>
{{/each}}
</tbody>

View File

@ -19,41 +19,17 @@
</div>
<div class='display-row username'>
<div class='field'>{{i18n 'user.username.title'}}</div>
<div class='value'>
{{#if editingUsername}}
{{text-field value=userUsernameValue autofocus="autofocus"}}
{{else}}
<span {{action "toggleUsernameEdit"}}>{{model.username}}</span>
{{/if}}
</div>
<div class='controls'>
{{#if editingUsername}}
{{d-button class="btn-default" action="saveUsername" label="admin.user_fields.save"}}
<a href {{action "toggleUsernameEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button class="btn-default" action="toggleUsernameEdit" icon="pencil"}}
{{/if}}
</div>
{{admin-editable-field name='user.username.title'
value=model.username
action=(action 'saveUsername')
editing=editingUsername}}
</div>
<div class='display-row'>
<div class='field'>{{i18n 'user.name.title'}}</div>
<div class='value'>
{{#if editingName}}
{{text-field value=userNameValue autofocus="autofocus"}}
{{else}}
<span {{action "toggleNameEdit"}}>{{model.name}}</span>
{{/if}}
</div>
<div class='controls'>
{{#if editingName}}
{{d-button class="btn-default" action="saveName" label="admin.user_fields.save"}}
<a href {{action "toggleNameEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button class="btn-default" action="toggleNameEdit" icon="pencil"}}
{{/if}}
</div>
{{admin-editable-field name='user.name.title'
value=model.name
action=(action 'saveName')
editing=editingName}}
</div>
{{plugin-outlet name="admin-user-below-names" args=(hash user=model) tagName='' connectorTagName=''}}
@ -130,22 +106,10 @@
</div>
<div class='display-row'>
<div class='field'>{{i18n 'user.title.title'}}</div>
<div class='value'>
{{#if editingTitle}}
{{text-field value=userTitleValue autofocus="autofocus"}}
{{else}}
<span {{action "toggleTitleEdit"}}>{{model.title}}&nbsp;</span>
{{/if}}
</div>
<div class='controls'>
{{#if editingTitle}}
{{d-button class="btn-default" action="saveTitle" label="admin.user_fields.save"}}
<a href {{action "toggleTitleEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
{{d-button class="btn-default" action="toggleTitleEdit" icon="pencil"}}
{{/if}}
</div>
{{admin-editable-field name='user.title.title'
value=model.title
action=(action 'saveTitle')
editing=editingTitle}}
</div>
<div class='display-row last-ip'>

View File

@ -7,9 +7,9 @@
<div class="admin-title">
<h2>{{title}}</h2>
{{#unless showEmails}}
{{#if canCheckEmails}}
<button {{action "showEmails"}} class="show-emails btn btn-default">{{i18n 'admin.users.show_emails'}}</button>
{{/unless}}
{{/if}}
</div>
<div class='username controls'>
{{text-field value=listFilter placeholder=searchHint}}

View File

@ -28,7 +28,8 @@ export default Em.Component.extend(UploadMixin, {
return {
type: "PUT",
dataType: "xml",
autoUpload: false
autoUpload: false,
multipart: false
};
},

View File

@ -0,0 +1,64 @@
import debounce from "discourse/lib/debounce";
import { searchForTerm } from "discourse/lib/search";
import { observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
loading: null,
noResults: null,
messages: null,
@observes("messageTitle")
messageTitleChanged() {
this.setProperties({
loading: true,
noResults: true,
selectedTopicId: null
});
this.search(this.get("messageTitle"));
},
@observes("messages")
messagesChanged() {
const messages = this.get("messages");
if (messages) {
this.set("noResults", messages.length === 0);
}
this.set("loading", false);
},
search: debounce(function(title) {
const currentTopicId = this.get("currentTopicId");
if (Em.isEmpty(title)) {
this.setProperties({ messages: null, loading: false });
return;
}
searchForTerm(title, {
typeFilter: "private_messages",
searchForId: true
}).then(results => {
if (results && results.posts && results.posts.length > 0) {
this.set(
"messages",
results.posts
.mapBy("topic")
.filter(t => t.get("id") !== currentTopicId)
);
} else {
this.setProperties({ messages: null, loading: false });
}
});
}, 300),
actions: {
chooseMessage(message) {
const messageId = Em.get(message, "id");
this.set("selectedTopicId", messageId);
Ember.run.next(() =>
$(`#choose-message-${messageId}`).prop("checked", "true")
);
return false;
}
}
});

View File

@ -25,5 +25,22 @@ export default Em.Component.extend(UploadMixin, {
uploadDone() {
bootbox.alert(I18n.t("user.invited.bulk_invite.success"));
}
},
uploadOptions() {
return { autoUpload: false };
},
_init: function() {
const $upload = this.$();
$upload.on("fileuploadadd", (e, data) => {
bootbox.confirm(
I18n.t("user.invited.bulk_invite.confirmation_message"),
I18n.t("cancel"),
I18n.t("go_ahead"),
result => (result ? data.submit() : data.abort())
);
});
}.on("didInsertElement")
});

View File

@ -15,6 +15,11 @@ export default Ember.Component.extend({
return publicExit && userIsGroupUser;
},
@computed("model.allow_membership_requests", "userIsGroupUser")
canRequestMembership(allowMembershipRequests, userIsGroupUser) {
return allowMembershipRequests && !userIsGroupUser;
},
@computed("model.is_group_user")
userIsGroupUser(isGroupUser) {
return !!isGroupUser;

View File

@ -60,7 +60,7 @@ export default Ember.Component.extend({
// on Desktop, shows the button at the beginning of the selection
// on Mobile, shows the button at the end of the selection
const isMobileDevice = this.site.isMobileDevice;
const { isIOS, isAndroid, isSafari, isOpera } = this.capabilities;
const { isIOS, isAndroid, isSafari, isOpera, isIE11 } = this.capabilities;
const showAtEnd = isMobileDevice || isIOS || isAndroid || isOpera;
// Don't mess with the original range as it results in weird behaviours
@ -88,7 +88,10 @@ export default Ember.Component.extend({
const parent = markerElement.parentNode;
parent.removeChild(markerElement);
// merge back all text nodes so they don't get messed up
parent.normalize();
if (!isIE11) {
// Skip this fix in IE11 - .normalize causes the selection to change
parent.normalize();
}
// work around Safari that would sometimes lose the selection
if (isSafari) {

View File

@ -786,7 +786,14 @@ export default Ember.Controller.extend({
// or get a draft sequence number
if (!opts.draft || opts.draftSequence === undefined) {
return Draft.get(opts.draftKey)
.then(data => self.confirmDraftAbandon(data))
.then(data => {
if (opts.skipDraftCheck) {
data.draft = undefined;
return data;
}
return self.confirmDraftAbandon(data);
})
.then(data => {
opts.draft = opts.draft || data.draft;

View File

@ -1,59 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { movePosts, mergeTopic } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
topicController: Ember.inject.controller("topic"),
saving: false,
selectedTopicId: null,
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
@computed("saving", "selectedTopicId")
buttonDisabled(saving, selectedTopicId) {
return saving || Ember.isEmpty(selectedTopicId);
},
@computed("saving")
buttonTitle(saving) {
return saving ? I18n.t("saving") : I18n.t("topic.merge_topic.title");
},
onShow() {
this.set("modal.modalClass", "split-modal");
},
actions: {
movePostsToExistingTopic() {
const topicId = this.get("model.id");
this.set("saving", true);
let promise = this.get("topicController.selectedAllPosts")
? mergeTopic(topicId, this.get("selectedTopicId"))
: movePosts(topicId, {
destination_topic_id: this.get("selectedTopicId"),
post_ids: this.get("topicController.selectedPostIds")
});
promise
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
Ember.run.next(() => DiscourseURL.routeTo(result.url));
})
.catch(() => {
this.flash(I18n.t("topic.merge_topic.error"));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -0,0 +1,155 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { movePosts, mergeTopic } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import { default as computed } from "ember-addons/ember-computed-decorators";
import { extractError } from "discourse/lib/ajax-error";
export default Ember.Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
canAddTags: Ember.computed.alias("site.can_create_tag"),
canTagMessages: Ember.computed.alias("site.can_tag_pms"),
selectedTopicId: null,
newTopic: Ember.computed.equal("selection", "new_topic"),
existingTopic: Ember.computed.equal("selection", "existing_topic"),
newMessage: Ember.computed.equal("selection", "new_message"),
existingMessage: Ember.computed.equal("selection", "existing_message"),
moveTypes: ["newTopic", "existingTopic", "newMessage", "existingMessage"],
participants: null,
topicController: Ember.inject.controller("topic"),
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
selectedAllPosts: Ember.computed.alias("topicController.selectedAllPosts"),
selectedPosts: Ember.computed.alias("topicController.selectedPosts"),
@computed("saving", "selectedTopicId", "topicName")
buttonDisabled(saving, selectedTopicId, topicName) {
return (
saving || (Ember.isEmpty(selectedTopicId) && Ember.isEmpty(topicName))
);
},
@computed(
"saving",
"newTopic",
"existingTopic",
"newMessage",
"existingMessage"
)
buttonTitle(saving, newTopic, existingTopic, newMessage, existingMessage) {
if (newTopic) {
return I18n.t("topic.split_topic.title");
} else if (existingTopic) {
return I18n.t("topic.merge_topic.title");
} else if (newMessage) {
return I18n.t("topic.move_to_new_message.title");
} else if (existingMessage) {
return I18n.t("topic.move_to_existing_message.title");
} else {
return I18n.t("saving");
}
},
onShow() {
this.setProperties({
"modal.modalClass": "move-to-modal",
saving: false,
selection: "new_topic",
categoryId: null,
topicName: "",
tags: null,
participants: null
});
const isPrivateMessage = this.get("model.isPrivateMessage");
const canSplitTopic = this.get("canSplitTopic");
if (isPrivateMessage) {
this.set("selection", canSplitTopic ? "new_message" : "existing_message");
} else if (!canSplitTopic) {
this.set("selection", "existing_topic");
}
},
@computed("selectedAllPosts", "selectedPosts", "selectedPosts.[]")
canSplitTopic(selectedAllPosts, selectedPosts) {
return (
!selectedAllPosts &&
selectedPosts.length > 0 &&
selectedPosts.sort((a, b) => a.post_number - b.post_number)[0]
.post_type === this.site.get("post_types.regular")
);
},
actions: {
performMove() {
this.get("moveTypes").forEach(type => {
if (this.get(type)) {
this.send("movePostsTo", type);
}
});
},
movePostsTo(type) {
this.set("saving", true);
const topicId = this.get("model.id");
let mergeOptions, moveOptions;
if (type === "existingTopic") {
mergeOptions = { destination_topic_id: this.get("selectedTopicId") };
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "existingMessage") {
mergeOptions = {
destination_topic_id: this.get("selectedTopicId"),
participants: this.get("participants"),
archetype: "private_message"
};
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "newTopic") {
mergeOptions = {};
moveOptions = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
category_id: this.get("categoryId"),
tags: this.get("tags")
};
} else {
mergeOptions = {};
moveOptions = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
tags: this.get("tags"),
archetype: "private_message"
};
}
const promise = this.get("topicController.selectedAllPosts")
? mergeTopic(topicId, mergeOptions)
: movePosts(topicId, moveOptions);
promise
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
DiscourseURL.routeTo(result.url);
})
.catch(xhr => {
this.flash(extractError(xhr, I18n.t("topic.move_to.error")));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -1,66 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { extractError } from "discourse/lib/ajax-error";
import { movePosts } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
canAddTags: Ember.computed.alias("site.can_create_tag"),
topicController: Ember.inject.controller("topic"),
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
@computed("saving", "topicName")
buttonDisabled(saving, topicName) {
return saving || Ember.isEmpty(topicName);
},
@computed("saving")
buttonTitle(saving) {
return saving ? I18n.t("saving") : I18n.t("topic.split_topic.action");
},
onShow() {
this.setProperties({
"modal.modalClass": "split-modal",
saving: false,
categoryId: null,
topicName: "",
tags: null
});
},
actions: {
movePostsToNewTopic() {
this.set("saving", true);
const options = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
category_id: this.get("categoryId"),
tags: this.get("tags")
};
movePosts(this.get("model.id"), options)
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
Ember.run.next(() => DiscourseURL.routeTo(result.url));
})
.catch(xhr => {
this.flash(extractError(xhr, I18n.t("topic.split_topic.error")));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -1113,22 +1113,6 @@ export default Ember.Controller.extend(BufferedContent, {
);
},
@computed(
"canMergeTopic",
"selectedAllPosts",
"selectedPosts",
"selectedPosts.[]"
)
canSplitTopic(canMergeTopic, selectedAllPosts, selectedPosts) {
return (
canMergeTopic &&
!selectedAllPosts &&
selectedPosts.length > 0 &&
selectedPosts.sort((a, b) => a.post_number - b.post_number)[0]
.post_type === 1
);
},
@computed("model.details.can_move_posts", "selectedPostsCount")
canMergeTopic(canMovePosts, selectedPostsCount) {
return canMovePosts && selectedPostsCount > 0;

View File

@ -5,6 +5,12 @@ import { iconHTML } from "discourse-common/lib/icon-library";
var get = Em.get,
escapeExpression = Handlebars.Utils.escapeExpression;
let _renderer = defaultCategoryLinkRenderer;
export function replaceCategoryLinkRenderer(fn) {
_renderer = fn;
}
function categoryStripe(color, classes) {
var style = color ? "style='background-color: #" + color + ";'" : "";
return "<span class='" + classes + "' " + style + "></span>";
@ -32,6 +38,43 @@ export function categoryBadgeHTML(category, opts) {
)
return "";
return _renderer(category, opts);
}
export function categoryLinkHTML(category, options) {
var categoryOptions = {};
// TODO: This is a compatibility layer with the old helper structure.
// Can be removed once we migrate to `registerUnbound` fully
if (options && options.hash) {
options = options.hash;
}
if (options) {
if (options.allowUncategorized) {
categoryOptions.allowUncategorized = true;
}
if (options.link !== undefined) {
categoryOptions.link = options.link;
}
if (options.extraClasses) {
categoryOptions.extraClasses = options.extraClasses;
}
if (options.hideParent) {
categoryOptions.hideParent = true;
}
if (options.categoryStyle) {
categoryOptions.categoryStyle = options.categoryStyle;
}
}
return new Handlebars.SafeString(
categoryBadgeHTML(category, categoryOptions)
);
}
registerUnbound("category-link", categoryLinkHTML);
function defaultCategoryLinkRenderer(category, opts) {
let description = get(category, "description_text");
let restricted = get(category, "read_restricted");
let url = opts.url
@ -103,36 +146,3 @@ export function categoryBadgeHTML(category, opts) {
extraClasses = categoryStyle ? categoryStyle + extraClasses : extraClasses;
return `<${tagName} class="badge-wrapper ${extraClasses}" ${href}>${html}</${tagName}>`;
}
export function categoryLinkHTML(category, options) {
var categoryOptions = {};
// TODO: This is a compatibility layer with the old helper structure.
// Can be removed once we migrate to `registerUnbound` fully
if (options && options.hash) {
options = options.hash;
}
if (options) {
if (options.allowUncategorized) {
categoryOptions.allowUncategorized = true;
}
if (options.link !== undefined) {
categoryOptions.link = options.link;
}
if (options.extraClasses) {
categoryOptions.extraClasses = options.extraClasses;
}
if (options.hideParent) {
categoryOptions.hideParent = true;
}
if (options.categoryStyle) {
categoryOptions.categoryStyle = options.categoryStyle;
}
}
return new Handlebars.SafeString(
categoryBadgeHTML(category, categoryOptions)
);
}
registerUnbound("category-link", categoryLinkHTML);

View File

@ -2,32 +2,63 @@ const OBSERVER_OPTIONS = {
rootMargin: "50%" // load images slightly before they're visible
};
// Min size in pixels for consideration for lazy loading
const MINIMUM_SIZE = 150;
const hiddenData = new WeakMap();
const LOADING_DATA =
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
// We hide an image by replacing it with a transparent gif
function hide(image) {
image.classList.add("d-lazyload");
image.classList.add("d-lazyload-hidden");
image.setAttribute("data-src", image.getAttribute("src"));
image.setAttribute(
"src",
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
);
hiddenData.set(image, {
src: image.src,
srcset: image.srcset,
width: image.width,
height: image.height
});
image.removeAttribute("srcset");
image.src = image.dataset.smallUpload || LOADING_DATA;
image.removeAttribute("data-small-upload");
}
// Restore an image from the `data-src` attribute
// Restore an image when onscreen
function show(image) {
let dataSrc = image.getAttribute("data-src");
if (dataSrc) {
image.setAttribute("src", dataSrc);
let imageData = hiddenData.get(image);
if (imageData) {
const copyImg = new Image();
copyImg.onload = () => {
image.src = copyImg.src;
if (copyImg.srcset) {
image.srcset = copyImg.srcset;
}
image.classList.remove("d-lazyload-hidden");
image.parentNode.removeChild(copyImg);
copyImg.onload = null;
};
copyImg.src = imageData.src;
copyImg.srcset = imageData.srcset || copyImg.srcset;
copyImg.style.position = "absolute";
copyImg.style.top = 0;
copyImg.style.left = 0;
copyImg.style.width = imageData.width;
copyImg.style.height = imageData.height;
image.parentNode.appendChild(copyImg);
} else {
image.classList.remove("d-lazyload-hidden");
}
}
export function setupLazyLoading(api) {
// Old IE don't support this API
if (!("IntersectionObserver" in window)) {
return;
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const { target } = entry;
@ -35,15 +66,19 @@ export function setupLazyLoading(api) {
if (entry.isIntersecting) {
show(target);
observer.unobserve(target);
} else {
// The Observer is triggered when entries are added. This allows
// us to hide things that start off screen.
hide(target);
}
});
}, OBSERVER_OPTIONS);
api.decorateCooked($post => {
$(".lightbox img", $post).each((_, $img) => observer.observe($img));
});
api.decorateCooked(
$post => {
$("img", $post).each((_, img) => {
if (img.width >= MINIMUM_SIZE && img.height >= MINIMUM_SIZE) {
hide(img);
observer.observe(img);
}
});
},
{ onlyStream: true }
);
}

View File

@ -46,9 +46,7 @@ export default function($elem) {
const href = item.el.data("download-href") || item.src;
let src = [
escapeExpression(item.el.attr("title")),
$("span.informations", item.el)
.text()
.replace("x", "&times;")
$("span.informations", item.el).text()
];
if (
!Discourse.SiteSettings.prevent_anons_from_downloading_files ||

View File

@ -28,6 +28,7 @@ import {
registerIconRenderer,
replaceIcon
} from "discourse-common/lib/icon-library";
import { replaceCategoryLinkRenderer } from "discourse/helpers/category-link";
import { addNavItem } from "discourse/models/nav-item";
import { replaceFormatter } from "discourse/lib/utilities";
import { modifySelectKit } from "select-kit/mixins/plugin-api";
@ -39,7 +40,7 @@ import Sharing from "discourse/lib/sharing";
import { addComposerUploadHandler } from "discourse/components/composer-editor";
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = "0.8.25";
const PLUGIN_API_VERSION = "0.8.26";
class PluginApi {
constructor(version, container) {
@ -775,6 +776,20 @@ class PluginApi {
addComposerUploadHandler(extensions, method) {
addComposerUploadHandler(extensions, method);
}
/**
* Registers a renderer that overrides the display of category links.
*
* Example:
*
* function testReplaceRenderer(category, opts) {
* return "Hello World";
* }
* api.replaceCategoryLinkRenderer(categoryIconsRenderer);
**/
replaceCategoryLinkRenderer(fn) {
replaceCategoryLinkRenderer(fn);
}
}
let _pluginv01;

View File

@ -158,7 +158,7 @@ const DiscourseURL = Ember.Object.extend({
return false;
}
if (a.host !== document.location.host) {
if (a.host && a.host !== document.location.host) {
document.location = a.href;
return false;
}

View File

@ -121,6 +121,10 @@ export default Ember.Mixin.create({
this.set("isFixed", true);
return this._show($target.text().replace(/^@/, ""), $target);
});
this.appEvents.on(`topic-header:trigger-${id}`, (username, $target) => {
return this._show(username, $target);
});
},
_positionCard(target) {

View File

@ -96,7 +96,9 @@ export default Em.Mixin.create({
});
$upload.on("fileuploadfail", (e, data) => {
displayErrorForUpload(data);
if (!data || (data && data.errorThrown !== "abort")) {
displayErrorForUpload(data);
}
reset();
});
}.on("didInsertElement"),

View File

@ -752,11 +752,10 @@ export function movePosts(topicId, data) {
);
}
export function mergeTopic(topicId, destinationTopicId) {
return ajax("/t/" + topicId + "/merge-topic", {
type: "POST",
data: { destination_topic_id: destinationTopicId }
}).then(moveResult);
export function mergeTopic(topicId, data) {
return ajax("/t/" + topicId + "/merge-topic", { type: "POST", data }).then(
moveResult
);
}
export default Topic;

View File

@ -716,6 +716,16 @@ User.reopenClass(Singleton, {
// TODO: Use app.register and junk Singleton
createCurrent() {
const userJson = PreloadStore.get("currentUser");
if (userJson && userJson.primary_group_id) {
const primaryGroup = userJson.groups.find(
group => group.id === userJson.primary_group_id
);
if (primaryGroup) {
userJson.primary_group_name = primaryGroup.name;
}
}
if (userJson) {
const store = Discourse.__container__.lookup("service:store");
return store.createRecord("user", userJson);

View File

@ -30,6 +30,7 @@ export default {
return p.toString() === "[object SafariRemoteNotification]";
})(!window["safari"] || safari.pushNotification);
caps.isChrome = !!window.chrome && !caps.isOpera;
caps.isIE11 = !!ua.match(/Trident.*rv\:11\./);
caps.canPasteImages = caps.isChrome || caps.isFirefox;
}

View File

@ -181,6 +181,7 @@ export default function() {
this.route("privacy", { path: "/privacy" });
this.route("guidelines", { path: "/guidelines" });
this.route("rules", { path: "/rules" });
this.route("conduct", { path: "/conduct" });
this.route("new-topic", { path: "/new-topic" });
this.route("new-message", { path: "/new-message" });

View File

@ -0,0 +1,3 @@
import staticRouteBuilder from "discourse/lib/static-route-builder";
export default staticRouteBuilder("conduct");

View File

@ -114,17 +114,13 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor("raw_email").loadRawEmail(model.get("id"));
},
mergeTopic() {
showModal("merge-topic", {
moveToTopic() {
showModal("move-to-topic", {
model: this.modelFor("topic"),
title: "topic.merge_topic.title"
title: "topic.move_to.title"
});
},
splitTopic() {
showModal("split-topic", { model: this.modelFor("topic") });
},
changeOwner() {
showModal("change-owner", {
model: this.modelFor("topic"),

View File

@ -1,3 +1,7 @@
<div class='ac-message'>
{{{i18n 'login.sent_activation_email_again' currentEmail=email}}}
{{#if email}}
{{{i18n 'login.sent_activation_email_again' currentEmail=email}}}
{{else}}
{{i18n 'login.sent_activation_email_again_generic'}}
{{/if}}
</div>

View File

@ -0,0 +1,22 @@
<label for='choose-message-title'>{{i18n 'choose_message.title.search'}}</label>
{{text-field value=messageTitle placeholderKey="choose_message.title.placeholder" id="choose-message-title"}}
{{#if loading}}
<p>{{i18n 'loading'}}</p>
{{else}}
{{#if noResults}}
<p>{{i18n 'choose_message.none_found'}}</p>
{{else}}
{{#each messages as |m|}}
<div class='controls existing-message'>
<label class='radio'>
<input type='radio' id="choose-message-{{unbound m.id}}" name='choose_message_id' {{action "chooseMessage" m}}>
<span class="message-title">
{{m.title}}
</span>
</label>
</div>
{{/each}}
{{/if}}
{{/if}}

View File

@ -10,7 +10,7 @@
icon="user-times"
label="groups.leave"
disabled=updatingMembership}}
{{else if model.allow_membership_requests}}
{{else if canRequestMembership}}
{{d-button action="showRequestMembershipForm"
class="group-index-request"
disabled=loading

View File

@ -1,13 +0,0 @@
{{#d-modal-body id='move-selected'}}
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-topic currentTopicId=model.id selectedTopicId=selectedTopicId}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToExistingTopic"}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -0,0 +1,112 @@
{{#d-modal-body id='move-selected'}}
{{#if model.isPrivateMessage}}
<div class="radios">
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-message">
{{radio-button id='move-to-new-message' name="move-to-entity" value="new_message" selection=selection}}
<b>{{i18n 'topic.move_to_new_message.radio_label'}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-message">
{{radio-button id='move-to-existing-message' name="move-to-entity" value="existing_message" selection=selection}}
<b>{{i18n 'topic.move_to_existing_message.radio_label'}}</b>
</label>
</div>
{{#if canSplitTopic}}
{{#if newMessage}}
<p>{{{i18n 'topic.move_to_new_message.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.move_to_new_message.message_title'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
{{#if canTagMessages}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if existingMessage}}
<p>{{{i18n 'topic.move_to_existing_message.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-message currentTopicId=model.id selectedTopicId=selectedTopicId}}
<label>{{i18n 'topic.move_to_new_message.participants'}}</label>
{{user-selector usernames=participants class="participant-selector"}}
</form>
{{/if}}
{{else}}
<div class="radios">
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-topic">
{{radio-button id='move-to-new-topic' name="move-to-entity" value="new_topic" selection=selection}}
<b>{{i18n 'topic.split_topic.radio_label'}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-topic">
{{radio-button id='move-to-existing-topic' name="move-to-entity" value="existing_topic" selection=selection}}
<b>{{i18n 'topic.merge_topic.radio_label'}}</b>
</label>
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-message">
{{radio-button id='move-to-new-message' name="move-to-entity" value="new_message" selection=selection}}
<b>{{i18n 'topic.move_to_new_message.radio_label'}}</b>
</label>
{{/if}}
</div>
{{#if existingTopic}}
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-topic currentTopicId=model.id selectedTopicId=selectedTopicId}}
</form>
{{/if}}
{{#if canSplitTopic}}
{{#if newTopic}}
<p>{{{i18n 'topic.split_topic.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.split_topic.topic_name'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
<label>{{i18n 'categories.category'}}</label>
{{category-chooser value=categoryId class="small"}}
{{#if canAddTags}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true categoryId=categoryId}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if canSplitTopic}}
{{#if newMessage}}
<p>{{{i18n 'topic.move_to_new_message.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.move_to_new_message.message_title'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
{{#if canTagMessages}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action=(action "performMove")}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -1,21 +0,0 @@
{{#d-modal-body id="move-selected" title="topic.split_topic.title"}}
{{{i18n 'topic.split_topic.instructions' count=selectedPostsCount}}}
<form>
<label>{{i18n 'topic.split_topic.topic_name'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
<label>{{i18n 'categories.category'}}</label>
{{category-chooser value=categoryId class="small"}}
{{#if canAddTags}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true categoryId=categoryId}}
{{/if}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToNewTopic"}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -12,12 +12,8 @@
{{d-button action="deleteSelected" icon="trash-o" label="topic.multi_select.delete" class="btn-danger"}}
{{/if}}
{{#if canSplitTopic}}
{{d-button action="splitTopic" icon="sign-out" label="topic.split_topic.action"}}
{{/if}}
{{#if canMergeTopic}}
{{d-button action="mergeTopic" icon="sign-out" label="topic.merge_topic.action"}}
{{d-button action=(route-action "moveToTopic") icon="sign-out" label="topic.move_to.action" class="move-to-topic"}}
{{/if}}
{{#if canChangeOwner}}

View File

@ -5,6 +5,54 @@ import DiscourseURL from "discourse/lib/url";
import RawHtml from "discourse/widgets/raw-html";
import renderTags from "discourse/lib/render-tags";
import { topicFeaturedLinkNode } from "discourse/lib/render-topic-featured-link";
import { avatarImg } from "discourse/widgets/post";
createWidget("topic-header-participant", {
tagName: "span",
buildClasses(attrs) {
return `trigger-${attrs.type}-card`;
},
html(attrs) {
const { user, group } = attrs;
let content, url;
if (attrs.type === "user") {
content = avatarImg("tiny", {
template: user.avatar_template,
username: user.username
});
url = user.get("path");
} else {
content = [iconNode("users")];
url = Discourse.getURL(`/groups/${group.name}`);
content.push(h("span", group.name));
}
return h(
"a.icon",
{
attributes: {
href: url,
"data-auto-route": true,
title: attrs.username
}
},
content
);
},
click(e) {
const $target = $(e.target);
this.appEvents.trigger(
`topic-header:trigger-${this.attrs.type}-card`,
this.attrs.username,
$target
);
e.preventDefault();
}
});
export default createWidget("header-topic-info", {
tagName: "div.extra-info-wrapper",
@ -73,6 +121,58 @@ export default createWidget("header-topic-info", {
extra.push(new RawHtml({ html: tags }));
}
if (showPM) {
const maxHeaderParticipants = extra.length > 0 ? 5 : 10;
const participants = [];
const topicDetails = topic.get("details");
const totalParticipants =
topicDetails.allowed_users.length +
topicDetails.allowed_groups.length;
topicDetails.allowed_users.some(user => {
if (participants.length >= maxHeaderParticipants) {
return true;
}
participants.push(
this.attach("topic-header-participant", {
type: "user",
user,
username: user.username
})
);
});
topicDetails.allowed_groups.some(group => {
if (participants.length >= maxHeaderParticipants) {
return true;
}
participants.push(
this.attach("topic-header-participant", {
type: "group",
group,
username: group.name
})
);
});
if (totalParticipants > maxHeaderParticipants) {
const remaining = totalParticipants - maxHeaderParticipants;
participants.push(
this.attach("link", {
className: "more-participants",
action: "jumpToTopPost",
href,
attributes: { "data-topic-id": topic.get("id") },
contents: () => `+${remaining}`
})
);
}
extra.push(h("div.topic-header-participants", participants));
}
extra = extra.concat(applyDecorators(this, "after-tags", attrs, state));
if (this.siteSettings.topic_featured_link_enabled) {

View File

@ -267,7 +267,7 @@ registerButton("delete", attrs => {
return {
id: "delete_topic",
action: "deletePost",
title: "topic.actions.delete",
title: "post.controls.delete_topic",
icon: "trash-o",
className: "delete"
};

View File

@ -211,15 +211,13 @@ export default createWidget("topic-admin-menu", {
});
}
if (this.currentUser.get("staff")) {
buttons.push({
className: "topic-admin-reset-bump-date",
buttonClass: "btn-default",
action: "resetBumpDate",
icon: "anchor",
label: "actions.reset_bump_date"
});
}
buttons.push({
className: "topic-admin-reset-bump-date",
buttonClass: "btn-default",
action: "resetBumpDate",
icon: "anchor",
label: "actions.reset_bump_date"
});
if (!isPrivateMessage) {
buttons.push({

View File

@ -344,6 +344,11 @@ export function setup(helper) {
helper.registerPlugin(md => {
const ruler = md.block.bbcode.ruler;
ruler.push("excerpt", {
tag: "excerpt",
wrap: "div.excerpt"
});
ruler.push("code", {
tag: "code",
replace: function(state, tagInfo, content) {

View File

@ -44,7 +44,7 @@ function addHashtag(buffer, matches, state) {
export function setup(helper) {
helper.registerPlugin(md => {
const rule = {
matcher: /#([\w-:]{1,101})/,
matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w-:]{1,101})/,
onMatch: addHashtag
};

View File

@ -193,8 +193,11 @@ export default DropdownSelectBoxComponent.extend({
}
const currentUser = Discourse.User.current();
const showToggleTopicBump =
currentUser &&
(currentUser.get("staff") || currentUser.trust_level === 4);
if (action === REPLY && currentUser && currentUser.get("staff")) {
if (action === REPLY && showToggleTopicBump) {
items.push({
name: I18n.t("composer.composer_actions.toggle_topic_bump.label"),
description: I18n.t("composer.composer_actions.toggle_topic_bump.desc"),
@ -298,6 +301,7 @@ export default DropdownSelectBoxComponent.extend({
options.action = action;
options.categoryId = this.get("composerModel.categoryId");
options.topicTitle = this.get("composerModel.title");
options.skipDraftCheck = true;
this._openComposer(options);
},

View File

@ -5,7 +5,7 @@ export default DropdownSelectBoxComponent.extend({
classNames: ["period-chooser"],
rowComponent: "period-chooser/period-chooser-row",
headerComponent: "period-chooser/period-chooser-header",
content: Ember.computed.alias("site.periods"),
content: Ember.computed.oneWay("site.periods"),
value: Ember.computed.alias("period"),
isHidden: Ember.computed.alias("showPeriods"),

View File

@ -25,10 +25,6 @@ export default Ember.Component.extend(
"isExpanded",
"isDisabled",
"isHidden",
"isAbove",
"isBelow",
"isLeftAligned",
"isRightAligned",
"hasSelection",
"hasReachedMaximum",
"hasReachedMinimum"

View File

@ -63,10 +63,9 @@ export default Ember.Mixin.create({
return this.$(this.filterInputSelector);
},
@on("didRender")
_adjustPosition() {
this._applyFixedPosition();
this._applyDirection();
this._applyFixedPosition();
this._positionWrapper();
},
@ -124,15 +123,27 @@ export default Ember.Mixin.create({
});
this.focusFilterOrHeader();
this.autoHighlight();
this._boundaryActionHandler("onExpand", this);
Ember.run.next(() => {
this._boundaryActionHandler("onExpand", this);
Ember.run.schedule("afterRender", () => {
if (!this.isDestroying && !this.isDestroyed) {
this._adjustPosition();
}
});
});
},
collapse() {
this.set("isExpanded", false);
Ember.run.next(() => {
Ember.run.schedule("afterRender", () => this._removeFixedPosition());
this._boundaryActionHandler("onCollapse", this);
Ember.run.schedule("afterRender", () => {
if (!this.isDestroying && !this.isDestroyed) {
this._removeFixedPosition();
}
});
});
},
@ -181,38 +192,61 @@ export default Ember.Mixin.create({
: windowWidth;
const bodyWidth = this._computedStyle(this.$body()[0], "width");
let marginToEdge;
let spaceToLeftEdge;
if (this.$scrollableParent().length) {
marginToEdge =
spaceToLeftEdge =
this.$().offset().left - this.$scrollableParent().offset().left;
} else {
marginToEdge = this.get("element").getBoundingClientRect().left;
spaceToLeftEdge = this.get("element").getBoundingClientRect().left;
}
const enoughMarginToOppositeEdge =
parentWidth - marginToEdge - bodyWidth + this.get("horizontalOffset") >
0;
if (enoughMarginToOppositeEdge) {
this.setProperties({ isLeftAligned: true, isRightAligned: false });
options.left = this.get("horizontalOffset");
options.right = "unset";
let isLeftAligned = true;
const spaceToRightEdge = parentWidth - spaceToLeftEdge;
const elementWidth = this.get("element").getBoundingClientRect().width;
if (spaceToRightEdge > spaceToLeftEdge + elementWidth) {
isLeftAligned = false;
}
if (isLeftAligned) {
this.$()
.addClass("is-left-aligned")
.removeClass("is-right-aligned");
if (this._isRTL()) {
options.right = this.get("horizontalOffset");
} else {
options.left =
-bodyWidth + elementWidth - this.get("horizontalOffset");
}
} else {
this.setProperties({ isLeftAligned: false, isRightAligned: true });
options.left = "unset";
options.right = this.get("horizontalOffset");
this.$()
.addClass("is-right-aligned")
.removeClass("is-left-aligned");
if (this._isRTL()) {
options.right =
-bodyWidth + elementWidth - this.get("horizontalOffset");
} else {
options.left = this.get("horizontalOffset");
}
}
}
const fullHeight =
this.get("verticalOffset") + bodyHeight + componentHeight;
const hasBelowSpace = $(window).height() - offsetBottom - fullHeight > 0;
const hasAboveSpace = offsetTop - fullHeight - discourseHeaderHeight > 0;
const hasBelowSpace = $(window).height() - offsetBottom - fullHeight >= -1;
const hasAboveSpace = offsetTop - fullHeight - discourseHeaderHeight >= -1;
const headerHeight = this._computedStyle(this.$header()[0], "height");
if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) {
this.setProperties({ isBelow: true, isAbove: false });
this.$()
.addClass("is-below")
.removeClass("is-above");
options.top = headerHeight + this.get("verticalOffset");
} else {
this.setProperties({ isBelow: false, isAbove: true });
this.$()
.addClass("is-above")
.removeClass("is-below");
options.bottom = headerHeight + this.get("verticalOffset");
}

View File

@ -151,19 +151,32 @@ export function createPreviewComponent(width, height, obj) {
);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55);
const headerFontSize = headerHeight / 44;
ctx.font = `${headerFontSize}em FontAwesome`;
ctx.fillText(
"\uf0c9",
width - avatarSize * 2 - headerMargin * 0.5,
avatarSize
const pathScale = headerHeight / 1200;
// search icon SVG path
const searchIcon = new Path2D(
"M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
);
ctx.fillText(
"\uf002",
// hamburger icon
const hamburgerIcon = new Path2D(
"M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
);
ctx.save(); // Save the previous state for translation and scale
ctx.translate(
width - avatarSize * 3 - headerMargin * 0.5,
avatarSize
avatarSize / 2
);
// need to scale paths otherwise they're too large
ctx.scale(pathScale, pathScale);
ctx.fill(searchIcon);
ctx.restore();
ctx.save();
ctx.translate(
width - avatarSize * 2 - headerMargin * 0.5,
avatarSize / 2
);
ctx.scale(pathScale, pathScale);
ctx.fill(hamburgerIcon);
ctx.restore();
},
drawPills(colors, headerHeight, opts) {
@ -176,37 +189,41 @@ export function createPreviewComponent(width, height, obj) {
const headerMargin = headerHeight * 0.2;
ctx.beginPath();
ctx.fillStyle = darkLightDiff(
colors.primary,
colors.secondary,
90,
-65
);
ctx.strokeStyle = colors.primary;
ctx.lineWidth = 0.5;
ctx.rect(
headerMargin,
headerHeight + headerMargin,
categoriesSize,
badgeHeight
);
ctx.fill();
ctx.stroke();
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px 'Arial'`;
ctx.fillStyle = colors.primary;
ctx.fillText(
"all categories",
headerMargin * 1.5,
headerHeight + headerMargin * 1.42 + fontSize
headerHeight + headerMargin * 1.4 + fontSize
);
ctx.font = "0.9em 'FontAwesome'";
ctx.fillStyle = colors.primary;
ctx.fillText(
"\uf0da",
categoriesSize - headerMargin / 4,
headerHeight + headerMargin * 1.6 + fontSize
const pathScale = badgeHeight / 1000;
// caret icon
const caretIcon = new Path2D(
"M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
);
ctx.save();
ctx.translate(
categoriesSize - headerMargin / 4,
headerHeight + headerMargin + badgeHeight / 4
);
ctx.scale(pathScale, pathScale);
ctx.fill(caretIcon);
ctx.restore();
const text = opts.categories ? "Categories" : "Latest";
const activeWidth = categoriesSize * (opts.categories ? 0.8 : 0.55);

View File

@ -470,7 +470,7 @@ $mobile-breakpoint: 700px;
nav {
display: inline-flex;
position: relative;
flex: 1;
flex: 1 0 0;
height: auto;
overflow: hidden;
padding: 0;
@ -892,7 +892,7 @@ table#user-badges {
.value-input {
box-sizing: border-box;
flex: 1;
flex: 1 0 0;
border-color: $primary-low;
cursor: pointer;
margin: 0;
@ -919,7 +919,7 @@ table#user-badges {
margin-left: -0.25em;
margin-top: -0.125em;
.new-value-input {
flex: 1;
flex: 1 0 0;
}
.value-input,
.new-value-input {

View File

@ -132,7 +132,7 @@
.mode {
display: inline-flex;
flex: 1;
flex: 1 0 0;
.mode-btn.is-current {
color: $tertiary;
@ -184,3 +184,9 @@
margin-right: auto;
}
}
.admin-report.storage-stats {
.main {
flex: 1 0 auto;
}
}

View File

@ -1,7 +1,7 @@
.admin-report {
.admin-report-counters {
display: grid;
flex: 1;
flex: 1 0 0;
grid-template-columns: 33% repeat(auto-fit, minmax(20px, 1fr));
grid-template-rows: repeat(auto-fit, minmax(32px, 1fr));
align-items: center;

View File

@ -13,20 +13,6 @@
height: 400px;
}
.reports-list {
list-style: none;
margin: 0;
.report {
padding-bottom: 0.5em;
margin-bottom: 0.5em;
.report-description {
margin: 0;
}
}
}
.report-container {
display: flex;
@ -41,7 +27,7 @@
.filters {
display: flex;
flex: 1;
flex: 1 0 0;
align-items: center;
flex-direction: column;
margin-left: 2em;

View File

@ -43,6 +43,10 @@
@include active-navigation-item;
}
&.dashboard-next-reports .navigation-item.reports {
@include active-navigation-item;
}
&.general .navigation-item.general {
@include active-navigation-item;
}
@ -191,7 +195,7 @@
display: flex;
border: 1px solid $primary-low;
.durability,
.storage-stats,
.last-dashboard-update {
flex: 1 1 50%;
box-sizing: border-box;
@ -199,7 +203,7 @@
padding: 0 1em;
}
.durability {
.storage-stats {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@ -213,15 +217,11 @@
.uploads p:last-of-type {
margin-bottom: 0;
}
.durability-title {
text-transform: capitalize;
}
}
@media screen and (max-width: 400px) {
flex-wrap: wrap;
.durability,
.storage-stats,
.last-dashboard-update {
flex: 1 1 100%;
text-align: left;
@ -286,7 +286,7 @@
.counters-list {
display: flex;
flex: 1;
flex: 1 0 0;
flex-direction: column;
.counters-header {
@ -530,3 +530,33 @@
grid-row-gap: 1em;
}
}
.dashboard-next-reports {
.reports-list {
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin: 0 -1.5%;
}
.report {
margin: 1.5%;
border: 1px solid $primary-low;
flex: 1 1 28%;
transition: box-shadow 0.25s;
min-width: 225px;
max-width: 550px;
a {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 1em;
.report-description {
color: $primary-high;
}
}
&:hover {
box-shadow: shadow("card");
}
}
}

View File

@ -133,22 +133,23 @@
.link-bottom-line {
font-size: $font-down-1;
a.badge-wrapper.box {
display: flex;
align-items: center;
a.badge-wrapper.box,
a.discourse-tag.box {
padding-top: 0;
padding-bottom: 0;
}
.discourse-tag.simple:after,
.discourse-tag.box {
margin-right: 0.25em;
}
}
.topic-featured-link {
padding-left: 5px;
}
span.badge-category {
.category-name {
max-width: 150px;
}
}
.topic-excerpt {
font-size: $font-down-1;
margin-top: 5px;

View File

@ -216,7 +216,7 @@
}
.category-chooser {
display: flex;
flex: 1;
flex: 1 0 auto;
width: auto;
}
}

View File

@ -60,6 +60,6 @@
// mobile styles
.mobile-view .edit-topic-timer-modal {
.select-kit.combo-box {
flex: 1;
flex: 1 0 0;
}
}

View File

@ -26,7 +26,7 @@ sup img.emoji {
.emoji-picker .categories-column {
display: flex;
flex-direction: column;
flex: 1;
flex: 1 0 0;
align-items: center;
justify-content: space-between;
border-right: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
@ -61,7 +61,7 @@ sup img.emoji {
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
padding: 0;
flex: 1;
flex: 1 0 0;
flex-direction: column;
}
@ -223,7 +223,7 @@ sup img.emoji {
.emoji-picker .filter input {
height: 24px;
margin: 0;
flex: 1;
flex: 1 0 0;
border: none;
box-shadow: none;
padding-right: 24px;
@ -247,7 +247,7 @@ sup img.emoji {
align-items: center;
justify-content: flex-start;
padding: 4px;
flex: 1;
flex: 1 0 0;
}
.emoji-picker .filter .clear-filter {

View File

@ -9,7 +9,6 @@
box-shadow: shadow("header");
> .wrap {
box-sizing: border-box;
width: 100%;
height: 100%;
.contents {
@ -298,3 +297,39 @@
min-width: 0;
}
}
.topic-header-participants {
&:not(:first-child) {
margin-left: 4px;
}
> span {
margin: 0 2px;
display: inline-block;
height: 20px;
}
.trigger-group-card {
height: 16px;
margin: 0 4px;
padding: 1px 4px;
border: 1px solid $primary-low;
border-radius: 0.25em;
align-items: center;
a {
color: $primary-high;
.d-icon {
margin-right: 4px;
}
}
}
.more-participants {
display: inline-block;
color: $header_primary-high;
line-height: 20px;
padding: 0 4px;
}
}

View File

@ -1,6 +1,7 @@
.lightbox-wrapper .lightbox {
position: relative;
display: inline-block;
overflow: hidden;
background: $primary-low;
&:hover .meta {
opacity: 0.9;
@ -9,10 +10,13 @@
}
.d-lazyload-hidden {
opacity: 0;
box-sizing: border-box;
}
.onebox img.d-lazyload-hidden {
border: 1px solid $primary-low;
}
.cooked img.d-lazyload {
transition: opacity 0.4s 0.75s ease;
}

View File

@ -567,7 +567,7 @@
.left,
.right {
flex: 1;
flex: 1 0 0;
}
.text {

View File

@ -10,7 +10,7 @@
margin-bottom: 1em;
.search-query {
flex: 1;
flex: 1 0 0;
margin: 0 0.5em 0 0;
}

View File

@ -124,14 +124,9 @@ $tag-color: $primary-medium;
}
.topic-list-item .discourse-tags {
display: inline-block;
display: inline-flex;
font-weight: normal;
font-size: $font-down-1;
.discourse-tag.box {
position: relative;
top: 2px;
}
}
.categories-list .topic-list-latest .discourse-tags {
@ -153,15 +148,16 @@ $tag-color: $primary-medium;
}
.discourse-tag.bullet {
margin-right: 0.25em;
margin-right: 0.5em;
display: inline-flex;
align-items: center;
&:before {
background: $primary-low-mid;
margin-right: 5px;
position: relative;
width: 8px;
height: 8px;
width: 9px;
height: 9px;
display: inline-block;
vertical-align: middle;
content: "";
}
}

View File

@ -396,7 +396,7 @@ aside.quote {
.remove-invited {
display: flex;
flex: 1;
flex: 1 0 0;
align-items: center;
justify-content: center;
box-sizing: border-box;
@ -419,7 +419,8 @@ aside.quote {
.avatar-flair-preview,
.user-card-avatar,
.topic-map .poster,
.user-profile-avatar {
.user-profile-avatar,
.user-image {
.avatar-flair {
display: flex;
align-items: center;
@ -433,7 +434,8 @@ aside.quote {
}
.topic-avatar .avatar-flair,
.avatar-flair-preview .avatar-flair,
.collapsed-info .user-profile-avatar .avatar-flair {
.collapsed-info .user-profile-avatar .avatar-flair,
.user-image .avatar-flair {
background-size: 20px 20px;
width: 20px;
height: 20px;

View File

@ -4,14 +4,12 @@
color: $primary;
border: 1px solid $primary-low;
line-height: $line-height-large;
display: inline-block;
display: inline-flex;
align-items: center;
background-color: $secondary;
margin: 0 0 3px;
.fa {
padding-right: 3px;
font-size: 1.4em;
vertical-align: bottom;
.d-icon {
margin-right: 0.25em;
}
img {

View File

@ -156,7 +156,7 @@
list-style-type: none;
}
a {
.btn {
margin-bottom: 10px;
line-height: $line-height-medium;
}

View File

@ -51,6 +51,7 @@
}
.badge-category-parent-bg,
.badge-category-bg {
flex: 0 0 auto;
width: 9px;
height: 9px;
margin-right: 5px;

View File

@ -11,6 +11,11 @@
overflow: visible;
}
// Fixes Edge bug with SVG elements not triggering click event
svg > use {
pointer-events: none;
}
// Stacked Icons
// Usage:
// <span class="fa-stack">

View File

@ -45,18 +45,6 @@
}
}
.avatar-flair {
background-repeat: no-repeat;
background-position: center;
position: absolute;
bottom: -2px;
right: -8px;
background-size: 18px 18px;
border-radius: 12px;
width: 24px;
height: 24px;
}
&.small {
width: 333px;
@media screen and (max-width: $small-width) {

View File

@ -230,7 +230,7 @@ $highlight-medium: dark-light-diff($highlight, $secondary, 50%, -55%);
$highlight-high: dark-light-diff($highlight, $secondary, -50%, -10%);
//danger
$danger-low: dark-light-diff($danger, $secondary, 85%, -85%);
$danger-low: dark-light-diff($danger, $secondary, 85%, -64%);
$danger-medium: dark-light-diff($danger, $secondary, 30%, -35%);
//success

View File

@ -21,6 +21,8 @@
}
.select-kit-header {
height: 30px;
.selected-name {
margin: 0;
border: 0;
@ -28,6 +30,7 @@
outline: none;
box-shadow: none;
cursor: pointer;
max-width: 250px;
}
}

View File

@ -17,7 +17,7 @@
.select-kit-filter {
border: 0;
flex: 1;
flex: 1 0 0;
margin: 1px;
}
@ -89,7 +89,7 @@
min-width: 50px;
padding: 0;
outline: none;
flex: 1;
flex: 1 0 0;
.filter-input,
.filter-input:focus {
@ -112,7 +112,7 @@
.selected-color {
.selected-color-wrapper {
display: flex;
flex: 1;
flex: 1 0 0;
flex-direction: column;
}
@ -137,7 +137,7 @@
padding: 2px 4px;
line-height: $line-height-medium;
display: flex;
flex: 1;
flex: 1 0 0;
align-items: center;
}
}

View File

@ -57,7 +57,7 @@
display: flex;
.period-title {
flex: 1;
flex: 1 0 0;
}
.date-section {

View File

@ -24,6 +24,8 @@
z-index: z("dropdown");
.select-kit-body {
-webkit-animation: fadein 0.25s;
animation: fadein 0.25s;
display: flex;
flex-direction: column;
left: 0;
@ -212,7 +214,7 @@
background: none;
margin: 0;
padding: 0;
flex: 1;
flex: 1 0 0;
outline: none;
border: 0;
border-radius: 0;

View File

@ -109,7 +109,7 @@
flex-flow: row wrap;
div.column {
flex: 1;
flex: 1 0 0;
flex-direction: row;
min-width: 300px;
}

View File

@ -41,7 +41,7 @@
}
}
.topic-stats {
flex: 1;
flex: 1 0 0;
text-align: right;
color: dark-light-choose($primary-medium, $secondary-high);
}

View File

@ -113,19 +113,28 @@
}
}
.split-modal {
.move-to-modal {
.modal-body {
position: relative;
height: 350px;
}
#move-selected {
width: 475px;
p {
margin-top: 0;
}
input[type="radio"] {
margin-right: 10px;
.radios {
margin-bottom: 10px;
display: flex;
flex-direction: row;
.radio-label {
display: inline-block;
padding-right: 15px;
}
}
button {
@ -142,9 +151,19 @@
width: 95%;
margin-top: 20px;
#split-topic-name,
#choose-topic-title {
#choose-topic-title,
#choose-message-title {
width: 100%;
}
.participant-selector {
width: 100%;
}
div.ac-wrap {
width: 100%;
margin-bottom: 9px;
}
}
}
}

View File

@ -517,9 +517,6 @@ video {
overflow: hidden;
text-overflow: ellipsis;
}
.topic-header-extra {
margin: 0 0 0 5px;
}
}
/* default docked header CSS for all topics, including those without categories */

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