Version bump

This commit is contained in:
Neil Lalonde 2021-07-15 14:56:25 -04:00
commit a615eecd36
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
1115 changed files with 62494 additions and 35070 deletions

View File

@ -26,10 +26,13 @@ jobs:
fail-fast: false
matrix:
build_type: [backend, frontend]
build_type: [backend, frontend, annotations]
target: [core, plugins]
postgres: ["13"]
redis: ["6.x"]
exclude:
- build_type: annotations
target: plugins
services:
postgres:
@ -131,3 +134,20 @@ jobs:
if: matrix.build_type == 'frontend' && matrix.target == 'plugins'
run: bin/rake plugin:qunit['*','1200000']
timeout-minutes: 30
- name: Check Annotations
if: matrix.build_type == 'annotations'
run: |
bin/rake annotate:ensure_all_indexes
bin/annotate --models --model-dir app/models
if [ ! -z "$(git status --porcelain app/models/)" ]; then
echo "Core annotations are not up to date. To resolve, run:"
echo " bin/rake annotate:clean"
echo
echo "Or manually apply the diff printed below:"
echo "---------------------------------------------"
git -c color.ui=always diff app/models/
exit 1
fi
timeout-minutes: 30

View File

@ -165,6 +165,8 @@ group :test, :development do
gem 'parallel_tests'
gem 'rswag-specs'
gem 'annotate'
end
group :development do
@ -173,8 +175,8 @@ group :development do
gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
gem 'binding_of_caller'
gem 'yaml-lint'
gem 'annotate'
gem 'discourse_dev'
gem 'discourse_dev_assets'
gem 'faker', "~> 2.16"
end
# this is an optional gem, it provides a high performance replacement

View File

@ -46,7 +46,7 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
@ -59,10 +59,10 @@ GEM
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.42.0)
aws-sdk-kms (1.44.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.90.0)
aws-sdk-s3 (1.96.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@ -115,8 +115,9 @@ GEM
railties (>= 3.1)
discourse-ember-source (3.12.2.3)
discourse-fonts (0.0.8)
discourse_dev (0.2.1)
discourse_dev_assets (0.0.3)
faker (~> 2.16)
literate_randomizer
docile (1.4.0)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
@ -128,30 +129,34 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
excon (0.82.0)
excon (0.84.0)
execjs (2.8.1)
exifr (1.3.9)
fabrication (2.22.0)
faker (2.18.0)
i18n (>= 1.6, < 2)
fakeweb (1.3.0)
faraday (1.4.2)
faraday (1.5.1)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
fast_blank (1.0.0)
fast_xs (0.8.0)
fastimage (2.2.4)
ffi (1.15.1)
ffi (1.15.3)
fspath (3.1.2)
gc_tracer (1.5.1)
globalid (0.4.2)
@ -172,7 +177,7 @@ GEM
image_size (>= 1.5, < 3)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
image_size (2.1.0)
image_size (2.1.1)
in_threads (1.5.4)
jmespath (1.4.0)
jquery-rails (4.4.0)
@ -198,6 +203,7 @@ GEM
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
literate_randomizer (0.4.0)
lograge (0.11.2)
actionpack (>= 4)
activesupport (>= 4)
@ -206,7 +212,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.9.6)
logster (2.9.7)
loofah (2.10.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -227,7 +233,7 @@ GEM
mini_suffix (0.3.2)
ffi (~> 1.9)
minitest (5.14.4)
mocha (1.12.0)
mocha (1.13.0)
mock_redis (0.28.0)
ruby2_keywords
msgpack (1.4.2)
@ -254,7 +260,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.11.5)
oj (3.12.1)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -284,7 +290,7 @@ GEM
parallel (1.20.1)
parallel_tests (3.7.0)
parallel
parser (3.0.1.1)
parser (3.0.2.0)
ast (~> 2.4.1)
pg (1.2.3)
progress (3.6.0)
@ -328,7 +334,7 @@ GEM
thor (~> 1.0)
rainbow (3.0.0)
raindrops (0.19.2)
rake (13.0.3)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
@ -337,7 +343,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.2.5)
redis (4.3.1)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
@ -349,7 +355,7 @@ GEM
rqrcode (2.0.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.0.0)
rqrcode_core (1.1.0)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
@ -379,7 +385,7 @@ GEM
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (1.16.0)
rubocop (1.18.3)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -388,12 +394,12 @@ GEM
rubocop-ast (>= 1.7.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0)
rubocop-ast (1.8.0)
parser (>= 3.0.1.1)
rubocop-discourse (2.4.2)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.3.0)
rubocop-rspec (2.4.0)
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
ruby-prof (1.4.3)
@ -402,7 +408,7 @@ GEM
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
ruby2_keywords (0.0.4)
rubyzip (2.3.0)
rubyzip (2.3.2)
sanitize (5.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
@ -419,8 +425,8 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
shoulda-matchers (5.0.0)
activesupport (>= 5.2.0)
sidekiq (6.2.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
@ -440,7 +446,7 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.17)
test-prof (1.0.5)
test-prof (1.0.6)
thor (1.1.0)
tilt (2.0.10)
tzinfo (2.0.4)
@ -504,12 +510,13 @@ DEPENDENCIES
discourse-ember-rails (= 0.18.6)
discourse-ember-source (~> 3.12.2)
discourse-fonts
discourse_dev
discourse_dev_assets
email_reply_trimmer
ember-handlebars-template (= 0.8.0)
excon
execjs
fabrication
faker (~> 2.16)
fakeweb
fast_blank
fast_xs

View File

@ -0,0 +1,22 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["penalty-history"],
@discourseComputed("user.penalty_counts.suspended")
suspendedCountClass(count) {
if (count > 0) {
return "danger";
}
return "";
},
@discourseComputed("user.penalty_counts.silenced")
silencedCountClass(count) {
if (count > 0) {
return "danger";
}
return "";
},
});

View File

@ -1,3 +1,4 @@
import Report from "admin/models/report";
import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce";
import loadScript from "discourse/lib/load-script";
@ -157,7 +158,7 @@ export default Component.extend({
gridLines: { display: false },
type: "time",
time: {
unit: this._unitForGrouping(options),
unit: Report.unitForGrouping(options.chartGrouping),
},
ticks: {
sampleSize: 5,
@ -179,62 +180,6 @@ export default Component.extend({
},
_applyChartGrouping(model, data, options) {
if (!options.chartGrouping || options.chartGrouping === "daily") {
return data;
}
if (
options.chartGrouping === "weekly" ||
options.chartGrouping === "monthly"
) {
const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month";
const kind = options.chartGrouping === "weekly" ? "week" : "month";
const startMoment = moment(model.start_date, "YYYY-MM-DD");
let currentIndex = 0;
let currentStart = startMoment.clone().startOf(isoKind);
let currentEnd = startMoment.clone().endOf(isoKind);
const transformedData = [
{
x: currentStart.format("YYYY-MM-DD"),
y: 0,
},
];
data.forEach((d) => {
let date = moment(d.x, "YYYY-MM-DD");
if (!date.isBetween(currentStart, currentEnd)) {
currentIndex += 1;
currentStart = currentStart.add(1, kind).startOf(isoKind);
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
}
if (transformedData[currentIndex]) {
transformedData[currentIndex].y += d.y;
} else {
transformedData[currentIndex] = {
x: d.x,
y: d.y,
};
}
});
return transformedData;
}
// ensure we return something if grouping is unknown
return data;
},
_unitForGrouping(options) {
switch (options.chartGrouping) {
case "monthly":
return "month";
case "weekly":
return "week";
default:
return "day";
}
return Report.collapse(model, data, options.chartGrouping);
},
});

View File

@ -1,3 +1,4 @@
import Report from "admin/models/report";
import Component from "@ember/component";
import discourseDebounce from "discourse-common/lib/debounce";
import loadScript from "discourse/lib/load-script";
@ -63,7 +64,7 @@ export default Component.extend({
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map((d) => Math.round(parseFloat(d.y))),
data: Report.collapse(model, cd.data),
backgroundColor: cd.color,
};
}),
@ -129,15 +130,14 @@ export default Component.extend({
},
},
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day",
unit: Report.unitForDatapoints(data.labels.length),
},
ticks: {
sampleSize: 5,

View File

@ -1,5 +1,5 @@
import EmberObject, { action, computed } from "@ember/object";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report";
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
import Component from "@ember/component";
import I18n from "I18n";
@ -21,26 +21,6 @@ const TABLE_OPTIONS = {
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: [
"isHidden:hidden",
@ -99,6 +79,10 @@ export default Component.extend({
}
this.set("endDate", endDate);
if (this.filters) {
this.set("currentMode", this.filters.mode);
}
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
@ -147,7 +131,7 @@ export default Component.extend({
return makeArray(modes).map((mode) => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
const cssClass = currentMode === mode ? `${base} btn-primary` : base;
return {
mode,
@ -196,15 +180,16 @@ export default Component.extend({
return reportKey;
},
@discourseComputed("reportOptions.chartGrouping")
chartGroupings(chartGrouping) {
chartGrouping = chartGrouping || "daily";
@discourseComputed("options.chartGrouping", "model.chartData.length")
chartGroupings(grouping, count) {
const options = ["daily", "weekly", "monthly"];
return ["daily", "weekly", "monthly"].map((id) => {
return options.map((id) => {
return {
id,
disabled: id === "daily" && count >= DAILY_LIMIT_DAYS,
label: `admin.dashboard.reports.${id}`,
class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}`,
class: `chart-grouping ${grouping === id ? "active" : "inactive"}`,
};
});
},
@ -240,6 +225,7 @@ export default Component.extend({
this.attrs.onRefresh({
type: this.get("model.type"),
mode: this.currentMode,
chartGrouping: options.chartGrouping,
startDate:
typeof options.startDate === "undefined"
@ -271,7 +257,7 @@ export default Component.extend({
},
@action
changeMode(mode) {
onChangeMode(mode) {
this.set("currentMode", mode);
this.send("refreshReport", {
@ -329,7 +315,7 @@ export default Component.extend({
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode),
options: this._buildOptions(currentMode, report),
});
},
@ -391,17 +377,19 @@ export default Component.extend({
return payload;
},
_buildOptions(mode) {
_buildOptions(mode, report) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
} else if (mode === "chart") {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
chartGrouping: this.get("reportOptions.chartGrouping"),
chartGrouping:
this.get("reportOptions.chartGrouping") ||
Report.groupingForDatapoints(report.chartData.length),
})
);
}
@ -414,7 +402,7 @@ export default Component.extend({
jsonReport.chartData = jsonReport.chartData.map((chartData) => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
data: chartData.data,
req: chartData.req,
label: chartData.label,
color: chartData.color,
@ -423,11 +411,6 @@ export default Component.extend({
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
@ -437,13 +420,6 @@ export default Component.extend({
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate,
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);

View File

@ -2,6 +2,7 @@ import Component from "@ember/component";
import { equal } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import I18n from "I18n";
export default Component.extend({
@ -16,7 +17,8 @@ export default Component.extend({
return replacement.split(",");
},
click() {
@action
deleteWord() {
this.word
.destroy()
.then(() => {

View File

@ -89,6 +89,7 @@ export default Component.extend({
word: "",
replacement: "",
formSubmitted: false,
selectedTags: [],
showMessage: true,
message: I18n.t("admin.watched_words.form.success"),
});

View File

@ -2,38 +2,98 @@ import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { extractError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
saving: false,
replaceBadgeOwners: false,
grantExistingHolders: false,
fileSelected: false,
unmatchedEntries: null,
resultsMessage: null,
success: false,
unmatchedEntriesCount: 0,
actions: {
massAward() {
const file = document.querySelector("#massAwardCSVUpload").files[0];
resetState() {
this.setProperties({
saving: false,
unmatchedEntries: null,
resultsMessage: null,
success: false,
unmatchedEntriesCount: 0,
});
this.send("updateFileSelected");
},
if (this.model && file) {
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
@discourseComputed("fileSelected", "saving")
massAwardButtonDisabled(fileSelected, saving) {
return !fileSelected || saving;
},
options.data.append("file", file);
options.data.append("replace_badge_owners", this.replaceBadgeOwners);
@discourseComputed("unmatchedEntriesCount", "unmatchedEntries.length")
unmatchedEntriesTruncated(unmatchedEntriesCount, length) {
return unmatchedEntriesCount && length && unmatchedEntriesCount > length;
},
this.set("saving", true);
@action
updateFileSelected() {
this.set(
"fileSelected",
!!document.querySelector("#massAwardCSVUpload")?.files?.length
);
},
ajax(`/admin/badges/award/${this.model.id}`, options)
.then(() => {
bootbox.alert(I18n.t("admin.badges.mass_award.success"));
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
}
},
@action
massAward() {
const file = document.querySelector("#massAwardCSVUpload").files[0];
if (this.model && file) {
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
options.data.append("file", file);
options.data.append("replace_badge_owners", this.replaceBadgeOwners);
options.data.append("grant_existing_holders", this.grantExistingHolders);
this.resetState();
this.set("saving", true);
ajax(`/admin/badges/award/${this.model.id}`, options)
.then(
({
matched_users_count: matchedCount,
unmatched_entries: unmatchedEntries,
unmatched_entries_count: unmatchedEntriesCount,
}) => {
this.setProperties({
resultsMessage: I18n.t("admin.badges.mass_award.success", {
count: matchedCount,
}),
success: true,
});
if (unmatchedEntries.length) {
this.setProperties({
unmatchedEntries,
unmatchedEntriesCount,
});
}
}
)
.catch((error) => {
this.setProperties({
resultsMessage: extractError(error),
success: false,
});
})
.finally(() => this.set("saving", false));
} else {
bootbox.alert(I18n.t("admin.badges.mass_award.aborted"));
}
},
});

View File

@ -2,8 +2,7 @@ import Controller from "@ember/controller";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
const { get } = Ember;
import { get } from "@ember/object";
export default Controller.extend({
filter: null,

View File

@ -2,7 +2,7 @@ import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
queryParams: ["start_date", "end_date", "filters", "chart_grouping"],
queryParams: ["start_date", "end_date", "filters", "chart_grouping", "mode"],
start_date: null,
end_date: null,
filters: null,

View File

@ -35,7 +35,6 @@ export default Controller.extend({
})
);
});
this.set("model", model);
},

View File

@ -12,6 +12,10 @@ export default Controller.extend(PenaltyController, {
this.setProperties({ silenceUntil: null, silencing: false });
},
finishedSetup() {
this.set("silenceUntil", this.user?.next_penalty);
},
@discourseComputed("silenceUntil", "reason", "silencing")
submitDisabled(silenceUntil, reason, silencing) {
return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1;

View File

@ -12,6 +12,10 @@ export default Controller.extend(PenaltyController, {
this.setProperties({ suspendUntil: null, suspending: false });
},
finishedSetup() {
this.set("suspendUntil", this.user?.next_penalty);
},
@discourseComputed("suspendUntil", "reason", "suspending")
submitDisabled(suspendUntil, reason, suspending) {
return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1;

View File

@ -18,33 +18,45 @@ export default Controller.extend(ModalFunctionality, {
)
matches(value, regexpString, words, isReplace, isTag, isLink) {
if (!value || !regexpString) {
return;
return [];
}
const regexp = new RegExp(regexpString, "ig");
const matches = value.match(regexp) || [];
if (isReplace || isLink) {
return matches.map((match) => ({
match,
replacement: words.find((word) =>
new RegExp(word.regexp, "ig").test(match)
).replacement,
}));
} else if (isTag) {
return matches.map((match) => {
const tags = new Set();
words.forEach((word) => {
if (new RegExp(word.regexp, "ig").test(match)) {
word.replacement.split(",").forEach((tag) => tags.add(tag));
}
});
return { match, tags: Array.from(tags) };
const matches = [];
words.forEach((word) => {
const regexp = new RegExp(word.regexp, "gi");
let match;
while ((match = regexp.exec(value)) !== null) {
matches.push({
match: match[1],
replacement: word.replacement,
});
}
});
}
return matches;
} else if (isTag) {
const matches = {};
words.forEach((word) => {
const regexp = new RegExp(word.regexp, "gi");
let match;
while ((match = regexp.exec(value)) !== null) {
if (!matches[match[1]]) {
matches[match[1]] = new Set();
}
return matches;
let tags = matches[match[1]];
word.replacement.split(",").forEach((tag) => {
tags.add(tag);
});
}
});
return Object.entries(matches).map((entry) => ({
match: entry[0],
tags: Array.from(entry[1]),
}));
} else {
return value.match(new RegExp(regexpString, "ig")) || [];
}
},
});

View File

@ -1,20 +1,5 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import ModalUpdateExistingUsers from "discourse/mixins/modal-update-existing-users";
export default Controller.extend(ModalFunctionality, {
onShow() {
this.set("updateExistingUsers", null);
},
actions: {
updateExistingUsers() {
this.set("updateExistingUsers", true);
this.send("closeModal");
},
cancel() {
this.set("updateExistingUsers", false);
this.send("closeModal");
},
},
});
export default Controller.extend(ModalFunctionality, ModalUpdateExistingUsers);

View File

@ -50,7 +50,7 @@ export default Mixin.create({
const vals = [],
translateNames = this.translate_names;
validValues.forEach((v) => {
(validValues || []).forEach((v) => {
if (v.name && v.name.length > 0 && translateNames) {
vals.addObject({ name: I18n.t(v.name), value: v.value });
} else {

View File

@ -1,6 +1,6 @@
import RestModel from "discourse/models/rest";
import { ajax } from "discourse/lib/ajax";
const { getProperties } = Ember;
import { getProperties } from "@ember/object";
export default RestModel.extend({
revert() {

View File

@ -503,7 +503,120 @@ const Report = EmberObject.extend({
},
});
export const WEEKLY_LIMIT_DAYS = 365;
export const DAILY_LIMIT_DAYS = 34;
function applyAverage(value, start, end) {
const count = end.diff(start, "day") + 1; // 1 to include start
return parseFloat((value / count).toFixed(2));
}
Report.reopenClass({
groupingForDatapoints(count) {
if (count < DAILY_LIMIT_DAYS) {
return "daily";
}
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
return "weekly";
}
if (count >= WEEKLY_LIMIT_DAYS) {
return "monthly";
}
},
unitForDatapoints(count) {
if (count >= DAILY_LIMIT_DAYS && count < WEEKLY_LIMIT_DAYS) {
return "week";
} else if (count >= WEEKLY_LIMIT_DAYS) {
return "month";
} else {
return "day";
}
},
unitForGrouping(grouping) {
switch (grouping) {
case "monthly":
return "month";
case "weekly":
return "week";
default:
return "day";
}
},
collapse(model, data, grouping) {
grouping = grouping || Report.groupingForDatapoints(data.length);
if (grouping === "daily") {
return data;
} else if (grouping === "weekly" || grouping === "monthly") {
const isoKind = grouping === "weekly" ? "isoWeek" : "month";
const kind = grouping === "weekly" ? "week" : "month";
const startMoment = moment(model.start_date, "YYYY-MM-DD");
let currentIndex = 0;
let currentStart = startMoment.clone().startOf(isoKind);
let currentEnd = startMoment.clone().endOf(isoKind);
const transformedData = [
{
x: currentStart.format("YYYY-MM-DD"),
y: 0,
},
];
let appliedAverage = false;
data.forEach((d) => {
const date = moment(d.x, "YYYY-MM-DD");
if (
!date.isSame(currentStart) &&
!date.isBetween(currentStart, currentEnd)
) {
if (model.average) {
transformedData[currentIndex].y = applyAverage(
transformedData[currentIndex].y,
currentStart,
currentEnd
);
appliedAverage = true;
}
currentIndex += 1;
currentStart = currentStart.add(1, kind).startOf(isoKind);
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
} else {
appliedAverage = false;
}
if (transformedData[currentIndex]) {
transformedData[currentIndex].y += d.y;
} else {
transformedData[currentIndex] = {
x: d.x,
y: d.y,
};
}
});
if (model.average && !appliedAverage) {
transformedData[currentIndex].y = applyAverage(
transformedData[currentIndex].y,
currentStart,
moment(model.end_date).subtract(1, "day") // remove 1 day as model end date is at 00:00 of next day
);
}
return transformedData;
}
// ensure we return something if grouping is unknown
return data;
},
fillMissingDates(report, options = {}) {
const dataField = options.dataField || "data";
const filledField = options.filledField || "data";

View File

@ -15,6 +15,7 @@ UserField.reopenClass({
UserFieldType.create({ id: "text" }),
UserFieldType.create({ id: "confirm" }),
UserFieldType.create({ id: "dropdown", hasOptions: true }),
UserFieldType.create({ id: "multiselect", hasOptions: true }),
];
}

View File

@ -9,4 +9,9 @@ export default Route.extend({
);
}
},
setupController(controller) {
this._super(...arguments);
controller.resetState();
},
});

View File

@ -6,6 +6,7 @@ export default DiscourseRoute.extend({
end_date: { refreshModel: true },
filters: { refreshModel: true },
chart_grouping: { refreshModel: true },
mode: { refreshModel: true },
},
model(params) {
@ -55,6 +56,7 @@ export default DiscourseRoute.extend({
onParamsChange(params) {
const queryParams = {
type: params.type,
mode: params.mode,
start_date: params.startDate
? params.startDate.toISOString(true).split("T")[0]
: null,

View File

@ -20,7 +20,7 @@ export default DiscourseRoute.extend({
search_type: params.searchType,
term: params.term,
},
}).then((json) => {
}).then(async (json) => {
// Add zero values for missing dates
if (json.term.data.length > 0) {
const startDate =
@ -31,7 +31,9 @@ export default DiscourseRoute.extend({
json.term.data = fillMissingDates(json.term.data, startDate, endDate);
}
if (json.term.search_result) {
json.term.search_result = translateResults(json.term.search_result);
json.term.search_result = await translateResults(
json.term.search_result
);
}
const model = EmberObject.create({ type: "search_log_term" });

View File

@ -48,7 +48,6 @@ export default Service.extend({
_showControlModal(type, user, opts) {
opts = opts || {};
let controller = showModal(`admin-${type}-user`, {
admin: true,
modalClass: `${type}-user-modal`,
@ -65,6 +64,8 @@ export default Service.extend({
before: opts.before,
successCallback: opts.successCallback,
});
controller.finishedSetup();
});
},

View File

@ -14,25 +14,62 @@
</div>
<div>
<h4>{{i18n "admin.badges.mass_award.upload_csv"}}</h4>
<input type="file" id="massAwardCSVUpload" accept=".csv">
<input type="file" id="massAwardCSVUpload" accept=".csv" onchange={{action "updateFileSelected"}}>
</div>
<div>
<label>
{{input type="checkbox" checked=replaceBadgeOwners}}
{{i18n "admin.badges.mass_award.replace_owners"}}
</label>
{{#if model.multiple_grant}}
<label class="grant-existing-holders">
{{input type="checkbox" checked=grantExistingHolders class="grant-existing-holders-checkbox"}}
{{i18n "admin.badges.mass_award.grant_existing_holders"}}
</label>
{{/if}}
</div>
{{d-button
class="btn-primary"
action=(action "massAward")
type="submit"
disabled=saving
disabled=massAwardButtonDisabled
icon="certificate"
label="admin.badges.mass_award.perform"}}
{{#link-to "adminBadges.index" class="btn btn-danger"}}
{{#link-to "adminBadges.index" class="btn btn-normal"}}
{{d-icon "times"}}
<span>{{i18n "cancel"}}</span>
{{/link-to}}
</form>
{{#if saving}}
{{i18n "uploading"}}
{{/if}}
{{#if resultsMessage}}
<p>
{{#if success}}
{{d-icon "check" class="bulk-award-status-icon success"}}
{{else}}
{{d-icon "times" class="bulk-award-status-icon failure"}}
{{/if}}
{{resultsMessage}}
</p>
{{#if unmatchedEntries.length}}
<p>
{{d-icon "exclamation-triangle" class="bulk-award-status-icon failure"}}
<span>
{{#if unmatchedEntriesTruncated}}
{{i18n "admin.badges.mass_award.csv_has_unmatched_users_truncated_list" count=unmatchedEntriesCount}}
{{else}}
{{i18n "admin.badges.mass_award.csv_has_unmatched_users"}}
{{/if}}
</span>
</p>
<ul>
{{#each unmatchedEntries as |entry|}}
<li>{{entry}}</li>
{{/each}}
</ul>
{{/if}}
{{/if}}
{{else}}
<span class="badge-required">{{i18n "admin.badges.mass_award.no_badge_selected"}}</span>
{{/if}}

View File

@ -0,0 +1,8 @@
<div class="suspended-count {{suspendedCountClass}}" title={{i18n "admin.user.last_six_months"}}>
<label>{{i18n "admin.user.suspended_count"}}</label>
<span>{{user.penalty_counts.suspended}}</span>
</div>
<div class="silenced-count {{silencedCountClass}}" title={{i18n "admin.user.last_six_months"}}>
<label>{{i18n "admin.user.silenced_count"}}</label>
<span>{{user.penalty_counts.silenced}}</span>
</div>

View File

@ -122,7 +122,7 @@
<div class="modes">
{{#each displayedModes as |displayedMode|}}
{{d-button
action=(action "changeMode")
action=(action "onChangeMode")
actionParam=displayedMode.mode
class=displayedMode.cssClass
icon=displayedMode.icon}}
@ -131,12 +131,18 @@
{{/if}}
{{#if isChartMode}}
{{#if model.average}}
<span class="average-chart">
{{i18n "admin.dashboard.reports.average_chart_label"}}
</span>
{{/if}}
<div class="chart-groupings">
{{#each chartGroupings as |chartGrouping|}}
{{d-button
label=chartGrouping.label
action=(action "changeGrouping" chartGrouping.id)
class=chartGrouping.class
disabled=chartGrouping.disabled
}}
{{/each}}
</div>

View File

@ -1,4 +1,4 @@
{{d-icon "times"}} {{word.word}}
<span role="button" onclick={{action "deleteWord"}} class="delete-word-record">{{d-icon "times"}}</span> {{word.word}}
{{#if (or isReplace isLink)}}
&rarr; <span class="replacement">{{word.replacement}}</span>
{{else if isTag}}

View File

@ -36,7 +36,7 @@
{{d-button
action=(action "editValue")
actionParam=data
icon="emoji-icon"
icon="discourse-emojis"
class="add-emoji-button d-editor-textarea-wrapper"
label="admin.site_settings.emoji_list.add_emoji_button.label"
}}

View File

@ -4,6 +4,7 @@
everyTag=true
options=(hash
allowAny=false
maximum=null
)
}}
<div class="desc">{{html-safe setting.description}}</div>

View File

@ -16,10 +16,12 @@
{{tag-chooser
id="watched-tag"
class="watched-word-input-field"
allowCreate=true
disabled=formSubmitted
tags=selectedTags
onChange=(action "changeSelectedTags")
options=(hash
allowAny=true
disabled=formSubmitted
)
}}
</div>
{{/if}}
@ -31,7 +33,7 @@
</div>
{{/if}}
{{d-button class="btn-default" action=(action "submit") disabled=formSubmitted label="admin.watched_words.form.add"}}
{{d-button class="btn btn-primary" action=(action "submit") disabled=formSubmitted label="admin.watched_words.form.add"}}
{{#if showMessage}}
<span class="success-message">{{message}}</span>

View File

@ -16,10 +16,7 @@
</div>
{{#each model.errors as |error|}}
<div class="alert alert-error">
<button type="button" class="close" data-dismiss="alert" aria-label={{i18n "modal.dismiss_error"}}>×</button>
{{error}}
</div>
<div class="alert alert-error">{{error}}</div>
{{/each}}
{{#unless model.supported}}

View File

@ -78,7 +78,10 @@
{{#if showPublicKey}}
<div class="public-key">
<div class="label">{{i18n "admin.customize.theme.public_key"}}</div>
{{textarea readonly=true value=publicKey}}
<div class="public-key-text-wrapper">
{{textarea class="public-key-value" readonly=true value=publicKey}}
{{copy-button selector="textarea.public-key-value"}}
</div>
</div>
{{else}}
{{#if privateChecked}}

View File

@ -5,6 +5,8 @@
<div class="alert alert-error">{{errorMessage}}</div>
{{/if}}
{{admin-penalty-history user=user}}
<div class="until-controls">
<label>
{{future-date-input

View File

@ -6,6 +6,8 @@
{{/if}}
{{#if user.canSuspend}}
{{admin-penalty-history user=user}}
<div class="until-controls">
<label>
{{future-date-input

View File

@ -19,18 +19,18 @@
{{text-field value=listFilter placeholder=searchHint}}
</div>
{{#load-more selector=".users-list tr" action=(action "loadMore")}}
{{#load-more class="users-list-container" selector=".users-list tr" action=(action "loadMore")}}
{{#if model}}
<table class="table users-list grid">
<thead>
{{table-header-toggle field="username" labelKey="username" order=order asc=asc}}
{{table-header-toggle field="email" labelKey="email" order=order asc=asc}}
{{table-header-toggle field="last_emailed" labelKey="admin.users.last_emailed" order=order asc=asc}}
{{table-header-toggle field="seen" labelKey="last_seen" order=order asc=asc}}
{{table-header-toggle field="topics_viewed" labelKey="admin.user.topics_entered" order=order asc=asc}}
{{table-header-toggle field="posts_read" labelKey="admin.user.posts_read_count" order=order asc=asc}}
{{table-header-toggle field="read_time" labelKey="admin.user.time_read" order=order asc=asc}}
{{table-header-toggle field="created" labelKey="created" order=order asc=asc}}
{{table-header-toggle field="username" labelKey="username" order=order asc=asc automatic=true}}
{{table-header-toggle class=(if showEmails "" "hidden") field="email" labelKey="email" order=order asc=asc automatic=true}}
{{table-header-toggle field="last_emailed" labelKey="admin.users.last_emailed" order=order asc=asc automatic=true}}
{{table-header-toggle field="seen" labelKey="last_seen" order=order asc=asc automatic=true}}
{{table-header-toggle field="topics_viewed" labelKey="admin.user.topics_entered" order=order asc=asc automatic=true}}
{{table-header-toggle field="posts_read" labelKey="admin.user.posts_read_count" order=order asc=asc automatic=true}}
{{table-header-toggle field="read_time" labelKey="admin.user.time_read" order=order asc=asc automatic=true}}
{{table-header-toggle field="created" labelKey="created" order=order asc=asc automatic=true}}
{{#if siteSettings.must_approve_users}}
<th>{{i18n "admin.users.approved"}}</th>
{{/if}}
@ -48,7 +48,7 @@
{{d-icon "far-envelope" title="user.staged" }}
{{/if}}
</td>
<td class="email">
<td class="email {{if showEmails "" "hidden"}}">
{{~user.email~}}
</td>
<td class="last-emailed">
@ -98,7 +98,6 @@
</tbody>
</table>
{{conditional-loading-spinner condition=refreshing}}
{{else}}
<p>{{i18n "search.no_results"}}</p>
{{/if}}

View File

@ -26,6 +26,10 @@
<p class="about">{{actionDescription}}</p>
{{#if siteSettings.watched_words_regular_expressions}}
<p>{{html-safe (i18n "admin.watched_words.regex_warning" basePath=(base-path))}}</p>
{{/if}}
{{watched-word-form
actionKey=actionNameKey
action=(action "recordAdded")

View File

@ -1,26 +1,28 @@
<div class="admin-controls">
<div class="controls">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action=(action "clearFilter") label="admin.watched_words.clear_filter"}}
<div class="admin-contents">
<div class="admin-controls">
<div class="controls">
{{d-button action=(action "toggleMenu") class="menu-toggle" icon="bars"}}
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
{{d-button action=(action "clearFilter") label="admin.watched_words.clear_filter"}}
</div>
</div>
</div>
<div class="admin-nav pull-left">
<ul class="nav nav-stacked">
{{#each model as |action|}}
<li class={{action.nameKey}}>
{{#link-to "adminWatchedWords.action" action.nameKey}}
{{action.name}}
{{#if action.words}}<span class="count">({{action.words.length}})</span>{{/if}}
{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class="admin-nav pull-left">
<ul class="nav nav-stacked">
{{#each model as |action|}}
<li class={{action.nameKey}}>
{{#link-to "adminWatchedWords.action" action.nameKey}}
{{action.name}}
{{#if action.words}}<span class="count">({{action.words.length}})</span>{{/if}}
{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class="admin-detail pull-left mobile-closed watched-words-detail">
{{outlet}}
</div>
<div class="admin-detail pull-left mobile-closed watched-words-detail">
{{outlet}}
</div>
<div class="clearfix"></div>
<div class="clearfix"></div>
</div>

View File

@ -69,3 +69,14 @@ export function setupS3CDN(configS3BaseUrl, configS3CDN) {
S3BaseUrl = configS3BaseUrl;
S3CDN = configS3CDN;
}
// We can use this to identify when navigating on the same host but outside of the
// prefix directory. For example from `/forum` to `/about-us` which is not discourse
export function samePrefix(url) {
if (baseUri === undefined) {
setPrefix($('meta[name="discourse-base-uri"]').attr("content") || "");
}
let origin = window.location.origin;
let cmp = url[0] === "/" ? baseUri || "/" : origin + baseUri || origin;
return url.indexOf(cmp) === 0;
}

View File

@ -34,6 +34,8 @@ var define, requirejs;
inject: Ember.inject.controller,
},
"@ember/debug": {
assert: Ember.assert,
runInDebug: Ember.runInDebug,
warn: Ember.warn,
},
"@ember/object": {

View File

@ -44,3 +44,24 @@ define("@popperjs/core", ["exports"], function (__exports__) {
__exports__.defaultModifiers = window.Popper.defaultModifiers;
__exports__.popperGenerator = window.Popper.popperGenerator;
});
define("@uppy/core", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.Core;
__exports__.Plugin = window.Uppy.Plugin;
});
define("@uppy/aws-s3", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.AwsS3;
});
define("@uppy/aws-s3-multipart", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.AwsS3Multipart;
});
define("@uppy/xhr-upload", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.XHRUpload;
});
define("@uppy/drop-target", ["exports"], function (__exports__) {
__exports__.default = window.Uppy.DropTarget;
});

View File

@ -4,17 +4,17 @@ import { observes } from "discourse-common/utils/decorators";
export default MountWidget.extend({
widget: "avatar-flair",
@observes("flairURL", "flairBgColor", "flairColor")
@observes("flairName", "flairUrl", "flairBgColor", "flairColor")
_rerender() {
this.queueRerender();
},
buildArgs() {
return {
primary_group_flair_url: this.flairURL,
primary_group_flair_bg_color: this.flairBgColor,
primary_group_flair_color: this.flairColor,
primary_group_name: this.groupName,
flair_name: this.flairName,
flair_url: this.flairUrl,
flair_bg_color: this.flairBgColor,
flair_color: this.flairColor,
};
},
});

View File

@ -5,7 +5,7 @@ import discourseComputed, {
import Component from "@ember/component";
import I18n from "I18n";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
const { makeArray } = Ember;
import { makeArray } from "discourse-common/lib/helpers";
export default Component.extend({
@discourseComputed("placeholderKey")

View File

@ -1,4 +1,6 @@
import {
authorizedExtensions,
authorizesAllExtensions,
authorizesOneOrMoreImageExtensions,
displayErrorForUpload,
getUploadMarkdown,
@ -200,6 +202,21 @@ export default Component.extend({
});
},
@discourseComputed()
acceptsAllFormats() {
return authorizesAllExtensions(this.currentUser.staff, this.siteSettings);
},
@discourseComputed()
acceptedFormats() {
const extensions = authorizedExtensions(
this.currentUser.staff,
this.siteSettings
);
return extensions.map((ext) => `.${ext}`).join();
},
@on("didInsertElement")
_composerEditorInit() {
const $input = $(this.element.querySelector(".d-editor-input"));
@ -635,6 +652,7 @@ export default Component.extend({
this.setProperties({
uploadProgress: 0,
isUploading: false,
isProcessingUpload: false,
isCancellable: false,
});
}
@ -672,6 +690,12 @@ export default Component.extend({
filename: data.files[data.index].name,
})}]()\n`
);
this.setProperties({
uploadProgress: 0,
isUploading: true,
isProcessingUpload: true,
isCancellable: false,
});
})
.on("fileuploadprocessalways", (e, data) => {
this.appEvents.trigger(
@ -681,6 +705,12 @@ export default Component.extend({
})}]()\n`,
""
);
this.setProperties({
uploadProgress: 0,
isUploading: false,
isProcessingUpload: false,
isCancellable: false,
});
});
$element.on("fileuploadpaste", (e) => {
@ -818,10 +848,12 @@ export default Component.extend({
});
if (this.site.mobileView) {
$("#reply-control .mobile-file-upload").on("click.uploader", function () {
// redirect the click on the hidden file input
$("#mobile-uploader").click();
});
const uploadButton = document.getElementById("mobile-file-upload");
uploadButton.addEventListener(
"click",
() => document.getElementById("file-uploader").click(),
false
);
}
},

View File

@ -1,8 +1,11 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import discourseDebounce from "discourse-common/lib/debounce";
export default Component.extend({
tagName: "",
copyIcon: "copy",
copyClass: "btn-primary",
@action
copy() {
@ -14,6 +17,17 @@ export default Component.extend({
if (this.copied) {
this.copied();
}
this.set("copyIcon", "check");
this.set("copyClass", "btn-primary ok");
discourseDebounce(() => {
if (this.isDestroying || this.isDestroyed) {
return;
}
this.set("copyIcon", "copy");
this.set("copyClass", "btn-primary");
}, 3000);
} catch (err) {}
},
});

View File

@ -3,6 +3,7 @@ import { scheduleOnce } from "@ember/runloop";
export default Component.extend({
classNames: ["modal-body"],
fixed: false,
submitOnEnter: true,
dismissable: true,
autoFocus: true,
@ -49,6 +50,7 @@ export default Component.extend({
"fixed",
"subtitle",
"rawSubtitle",
"submitOnEnter",
"dismissable",
"headerClass",
"autoFocus"
@ -60,7 +62,12 @@ export default Component.extend({
const modalAlert = document.getElementById("modal-alert");
if (modalAlert) {
modalAlert.style.display = "none";
modalAlert.classList.remove("alert-info", "alert-error", "alert-success");
modalAlert.classList.remove(
"alert-error",
"alert-info",
"alert-success",
"alert-warning"
);
}
},

View File

@ -19,6 +19,7 @@ export default Component.extend({
"role",
"ariaLabelledby:aria-labelledby",
],
submitOnEnter: true,
dismissable: true,
title: null,
subtitle: null,
@ -48,7 +49,7 @@ export default Component.extend({
@on("didInsertElement")
setUp() {
$("html").on("keyup.discourse-modal", (e) => {
//only respond to events when the modal is visible
// only respond to events when the modal is visible
if (!this.element.classList.contains("hidden")) {
if (e.which === 27 && this.dismissable) {
next(() => this.attrs.closeModal("initiatedByESC"));
@ -70,6 +71,10 @@ export default Component.extend({
},
triggerClickOnEnter(e) {
if (!this.submitOnEnter) {
return false;
}
// skip when in a form or a textarea element
if (
e.target.closest("form") ||
@ -124,6 +129,10 @@ export default Component.extend({
this.set("subtitle", null);
}
if ("submitOnEnter" in data) {
this.set("submitOnEnter", data.submitOnEnter);
}
if ("dismissable" in data) {
this.set("dismissable", data.dismissable);
} else {

View File

@ -2,16 +2,109 @@ import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
classNames: ["directory-table-container"],
lastScrollPosition: 0,
ticking: false,
_topHorizontalScrollBar: null,
_tableContainer: null,
_table: null,
_fakeScrollContent: null,
didInsertElement() {
this._super(...arguments);
this.setProperties({
_tableContainer: this.element.querySelector(".directory-table-container"),
_topHorizontalScrollBar: this.element.querySelector(
".directory-table-top-scroll"
),
_fakeScrollContent: this.element.querySelector(
".directory-table-top-scroll-fake-content"
),
_table: this.element.querySelector(".directory-table"),
});
this._tableContainer.addEventListener("scroll", this.onBottomScroll);
this._topHorizontalScrollBar.addEventListener("scroll", this.onTopScroll);
// Set active header might have already scrolled the _tableContainer.
// Call onHorizontalScroll manually to scroll the _topHorizontalScrollBar
this.onResize();
this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
window.addEventListener("resize", this.onResize);
},
@action
onResize() {
if (
this._tableContainer.getBoundingClientRect().bottom < window.innerHeight
) {
// Bottom of the table is visible. Hide the scrollbar
this._fakeScrollContent.style.height = 0;
} else {
this._fakeScrollContent.style.width = `${this._table.offsetWidth}px`;
this._fakeScrollContent.style.height = "1px";
}
},
@action
onTopScroll() {
this.onHorizontalScroll(this._topHorizontalScrollBar, this._tableContainer);
},
@action
onBottomScroll() {
this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
},
@action
onHorizontalScroll(primary, replica) {
if (
this.isDestroying ||
this.isDestroyed ||
this.lastScrollPosition === primary.scrollLeft
) {
return;
}
this.set("lastScrollPosition", primary.scrollLeft);
if (!this.ticking) {
window.requestAnimationFrame(() => {
if (!this.isDestroying && !this.isDestroyed) {
replica.scrollLeft = this.lastScrollPosition;
this.set("ticking", false);
}
});
this.set("ticking", true);
}
},
willDestoryElement() {
this._tableContainer.removeEventListener("scroll", this.onBottomScroll);
this._topHorizontalScrollBar.removeEventListener(
"scroll",
this.onTopScroll
);
window.removeEventListener("resize", this.onResize);
},
@action
setActiveHeader(header) {
// After render, scroll table left to ensure the order by column is visible
if (!this._tableContainer) {
this.set(
"_tableContainer",
document.querySelector(".directory-table-container")
);
}
const scrollPixels =
header.offsetLeft + header.offsetWidth + 10 - this.element.offsetWidth;
header.offsetLeft +
header.offsetWidth +
10 -
this._tableContainer.offsetWidth;
if (scrollPixels > 0) {
this.element.scrollLeft = scrollPixels;
this._tableContainer.scrollLeft = scrollPixels;
}
},
});

View File

@ -14,7 +14,12 @@ import I18n from "I18n";
import { action } from "@ember/object";
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
import { now, startOfDay, thisWeekend } from "discourse/lib/time-utils";
import {
MOMENT_MONDAY,
now,
startOfDay,
thisWeekend,
} from "discourse/lib/time-utils";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import Mousetrap from "mousetrap";
@ -82,22 +87,22 @@ export default Component.extend({
{
icon: "bed",
id: "this_weekend",
label: "topic.auto_update_input.this_weekend",
label: "time_shortcut.this_weekend",
time: thisWeekend(),
timeFormatKey: "dates.time_short_day",
},
{
icon: "far-clock",
id: "two_weeks",
label: "topic.auto_update_input.two_weeks",
time: startOfDay(now().add(2, "weeks")),
label: "time_shortcut.two_weeks",
time: startOfDay(now().add(2, "weeks").day(MOMENT_MONDAY)),
timeFormatKey: "dates.long_no_year",
},
{
icon: "far-calendar-plus",
id: "six_months",
label: "topic.auto_update_input.six_months",
time: startOfDay(now().add(6, "months")),
label: "time_shortcut.six_months",
time: startOfDay(now().add(6, "months").startOf("month")),
timeFormatKey: "dates.long_no_year",
},
];
@ -105,7 +110,7 @@ export default Component.extend({
@discourseComputed
hiddenTimeShortcutOptions() {
return ["none", "start_of_next_business_week"];
return ["none"];
},
isCustom: equal("timerType", "custom"),

View File

@ -45,7 +45,9 @@ export default Component.extend({
this.set("recentEmojis", this.emojiStore.favorites);
this.set("selectedDiversity", this.emojiStore.diversity);
this._sectionObserver = this._setupSectionObserver();
if ("IntersectionObserver" in window) {
this._sectionObserver = this._setupSectionObserver();
}
},
didInsertElement() {
@ -107,10 +109,6 @@ export default Component.extend({
);
}
emojiPicker
.querySelectorAll(".emojis-container .section .section-header")
.forEach((p) => this._sectionObserver.observe(p));
// this is a low-tech trick to prevent appending hundreds of emojis
// of blocking the rendering of the picker
later(() => {
@ -123,6 +121,12 @@ export default Component.extend({
) {
const filter = emojiPicker.querySelector("input.filter");
filter && filter.focus();
if (this._sectionObserver) {
emojiPicker
.querySelectorAll(".emojis-container .section .section-header")
.forEach((p) => this._sectionObserver.observe(p));
}
}
if (this.selectedDiversity !== 0) {
@ -216,23 +220,22 @@ export default Component.extend({
@action
onFilter(event) {
const emojiPickerArea = document.querySelector(".emoji-picker-emoji-area");
const emojisContainer = emojiPickerArea.querySelector(".emojis-container");
const results = emojiPickerArea.querySelector(".results");
const emojiPicker = document.querySelector(".emoji-picker");
const results = document.querySelector(".emoji-picker-emoji-area .results");
results.innerHTML = "";
if (event.target.value) {
results.innerHTML = emojiSearch(event.target.value.toLowerCase(), {
maxResults: 10,
maxResults: 20,
diversity: this.emojiStore.diversity,
})
.map(this._replaceEmoji)
.join("");
emojisContainer.style.visibility = "hidden";
emojiPicker.classList.add("has-filter");
results.scrollIntoView();
} else {
emojisContainer.style.visibility = "visible";
emojiPicker.classList.remove("has-filter");
}
},

View File

@ -14,9 +14,10 @@ export default Component.extend({
displayLabel: null,
labelClasses: null,
timeInputDisabled: empty("date"),
init() {
this._super(...arguments);
if (this.input) {
const datetime = moment(this.input);
this.setProperties({
@ -27,8 +28,6 @@ export default Component.extend({
}
},
timeInputDisabled: empty("date"),
@observes("date", "time")
_updateInput() {
if (!this.date) {

View File

@ -67,8 +67,8 @@ export default Component.extend({
},
@discourseComputed("model.flair_url")
flairImageUrl(flairURL) {
return flairURL && flairURL.match(/\//) ? flairURL : null;
flairImageUrl(flairUrl) {
return flairUrl && flairUrl.includes("/") ? flairUrl : null;
},
@discourseComputed(
@ -78,7 +78,7 @@ export default Component.extend({
"model.flairHexColor"
)
flairPreviewStyle(
flairURL,
flairUrl,
flairPreviewImage,
flairBackgroundHexColor,
flairHexColor
@ -86,7 +86,7 @@ export default Component.extend({
let style = "";
if (flairPreviewImage) {
style += `background-image: url(${escapeExpression(flairURL)});`;
style += `background-image: url(${escapeExpression(flairUrl)});`;
}
if (flairBackgroundHexColor) {

View File

@ -4,10 +4,12 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { popupAutomaticMembershipAlert } from "discourse/controllers/groups-new";
import showModal from "discourse/lib/show-modal";
export default Component.extend({
saving: null,
disabled: false,
updateExistingUsers: null,
@discourseComputed("saving")
savingText(saving) {
@ -28,14 +30,37 @@ export default Component.extend({
group.automatic_membership_email_domains
);
const opts = {};
if (this.updateExistingUsers !== null) {
opts.update_existing_users = this.updateExistingUsers;
}
return group
.save()
.save(opts)
.then((data) => {
if (data.user_count) {
const controller = showModal("group-default-notifications", {
model: {
count: data.user_count,
},
});
controller.set("onClose", () => {
this.updateExistingUsers = controller.updateExistingUsers;
this.send("save");
});
return;
}
if (data.route_to) {
DiscourseURL.routeTo(data.route_to);
}
this.set("saved", true);
this.setProperties({
saved: true,
updateExistingUsers: null,
});
if (this.afterSave) {
this.afterSave();

View File

@ -1,19 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
@discourseComputed("model.imap_mailboxes")
mailboxes(imapMailboxes) {
return imapMailboxes.map((mailbox) => ({ name: mailbox, value: mailbox }));
},
@discourseComputed("model.imap_old_emails")
oldEmails(oldEmails) {
return oldEmails || 0;
},
@discourseComputed("model.imap_old_emails", "model.imap_new_emails")
totalEmails(oldEmails, newEmails) {
return (oldEmails || 0) + (newEmails || 0);
},
});

View File

@ -250,7 +250,9 @@ export default Component.extend({
@discourseComputed("topic.{id,slug}", "quoteState")
shareUrl(topic, quoteState) {
return getAbsoluteURL(postUrl(topic.slug, topic.id, quoteState.postId));
const postId = quoteState.postId;
const postNumber = topic.postStream.findLoadedPost(postId).post_number;
return getAbsoluteURL(postUrl(topic.slug, topic.id, postNumber));
},
@discourseComputed("topic.details.can_create_post", "composerVisible")

View File

@ -49,7 +49,9 @@ export default MountWidget.extend({
"selectedPostsCount",
"searchService",
"showReadIndicator",
"streamFilters"
"streamFilters",
"lastReadPostNumber",
"highestPostNumber"
);
},

View File

@ -1,7 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
classNameBindings: [":social-link"],
tagName: "",
actions: {
share: function (source) {
this.action(source);

View File

@ -29,7 +29,9 @@ const SiteHeaderComponent = MountWidget.extend(
@observes(
"currentUser.unread_notifications",
"currentUser.unread_high_priority_notifications",
"currentUser.reviewable_count"
"currentUser.reviewable_count",
"session.defaultColorSchemeIsDark",
"session.darkModeAvailable"
)
notificationsChanged() {
this.queueRerender();
@ -87,6 +89,7 @@ const SiteHeaderComponent = MountWidget.extend(
const menuPanels = document.querySelectorAll(".menu-panel");
const menuOrigin = this._panMenuOrigin;
menuPanels.forEach((panel) => {
panel.classList.remove("moving");
if (this._shouldMenuClose(event, menuOrigin)) {
this._animateClosing(panel, menuOrigin);
} else {
@ -129,6 +132,10 @@ const SiteHeaderComponent = MountWidget.extend(
) {
e.originalEvent.preventDefault();
this._isPanning = true;
const panel = document.querySelector(".menu-panel");
if (panel) {
panel.classList.add("moving");
}
} else {
this._isPanning = false;
}

View File

@ -9,6 +9,7 @@ export default Component.extend({
chevronIcon: null,
columnIcon: null,
translated: false,
automatic: false,
onActiveRender: null,
toggleProperties() {
@ -31,6 +32,9 @@ export default Component.extend({
},
didReceiveAttrs() {
this._super(...arguments);
if (!this.automatic && !this.translated) {
this.set("labelKey", this.field);
}
this.set("id", `table-header-toggle-${this.field.replace(/\s/g, "")}`);
this.toggleChevron();
},

View File

@ -32,10 +32,6 @@ const BINDINGS = {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.TOMORROW],
},
"n w": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.NEXT_WEEK],
},
"n b w": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK],

View File

@ -40,8 +40,6 @@ export default Component.extend({
classNameBindings: [":topic-list-item", "unboundClassNames", "topic.visited"],
attributeBindings: ["data-topic-id", "role", "ariaLevel:aria-level"],
"data-topic-id": alias("topic.id"),
role: "heading",
ariaLevel: "2",
didReceiveAttrs() {
this._super(...arguments);
@ -144,8 +142,8 @@ export default Component.extend({
classes.push("unseen-topic");
}
if (topic.get("displayNewPosts")) {
classes.push("new-posts");
if (topic.unread_posts) {
classes.push("unread-posts");
}
["liked", "archived", "bookmarked", "pinned", "closed"].forEach((name) => {

View File

@ -1,13 +1,16 @@
import Component from "@ember/component";
import I18n from "I18n";
import { or } from "@ember/object/computed";
export default Component.extend({
tagName: "span",
classNameBindings: [":topic-post-badges"],
rerenderTriggers: ["url", "unread", "newPosts", "unseen"],
rerenderTriggers: ["url", "unread", "newPosts", "unreadPosts", "unseen"],
newDotText: null,
init() {
this._super(...arguments);
this.set(
"newDotText",
this.currentUser && this.currentUser.trust_level > 0
@ -15,4 +18,6 @@ export default Component.extend({
: I18n.t("filters.new.lower_title")
);
},
displayUnreadPosts: or("newPosts", "unreadPosts"),
});

View File

@ -0,0 +1,133 @@
import Component from "@ember/component";
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { getURLWithCDN } from "discourse-common/lib/get-url";
import { isEmpty } from "@ember/utils";
import lightbox from "discourse/lib/lightbox";
import { next } from "@ember/runloop";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Component.extend(UppyUploadMixin, {
classNames: ["image-uploader"],
loadingLightbox: false,
init() {
this._super(...arguments);
this._applyLightbox();
},
willDestroyElement() {
this._super(...arguments);
const elem = $("a.lightbox");
if (elem && typeof elem.magnificPopup === "function") {
$("a.lightbox").magnificPopup("close");
}
},
@discourseComputed("imageUrl", "placeholderUrl")
showingPlaceholder(imageUrl, placeholderUrl) {
return !imageUrl && placeholderUrl;
},
@discourseComputed("placeholderUrl")
placeholderStyle(url) {
if (isEmpty(url)) {
return "".htmlSafe();
}
return `background-image: url(${url})`.htmlSafe();
},
@discourseComputed("imageUrl")
imageCDNURL(url) {
if (isEmpty(url)) {
return "".htmlSafe();
}
return getURLWithCDN(url);
},
@discourseComputed("imageCDNURL")
backgroundStyle(url) {
return `background-image: url(${url})`.htmlSafe();
},
@discourseComputed("imageUrl")
imageBaseName(imageUrl) {
if (isEmpty(imageUrl)) {
return;
}
return imageUrl.split("/").slice(-1)[0];
},
validateUploadedFilesOptions() {
return { imagesOnly: true };
},
uploadDone(upload) {
this.setProperties({
imageUrl: upload.url,
imageId: upload.id,
imageFilesize: upload.human_filesize,
imageFilename: upload.original_filename,
imageWidth: upload.width,
imageHeight: upload.height,
});
this._applyLightbox();
if (this.onUploadDone) {
this.onUploadDone(upload);
}
},
_openLightbox() {
next(() =>
$(this.element.querySelector("a.lightbox")).magnificPopup("open")
);
},
_applyLightbox() {
if (this.imageUrl) {
next(() => lightbox(this.element, this.siteSettings));
}
},
actions: {
toggleLightbox() {
if (this.imageFilename) {
this._openLightbox();
} else {
this.set("loadingLightbox", true);
ajax(`/uploads/lookup-metadata`, {
type: "POST",
data: { url: this.imageUrl },
})
.then((json) => {
this.setProperties({
imageFilename: json.original_filename,
imageFilesize: json.human_filesize,
imageWidth: json.width,
imageHeight: json.height,
});
this._openLightbox();
this.set("loadingLightbox", false);
})
.catch(popupAjaxError);
}
},
trash() {
this.setProperties({ imageUrl: null, imageId: null });
// uppy needs to be reset to allow for more uploads
this._reset();
if (this.onUploadDeleted) {
this.onUploadDeleted();
}
},
},
});

View File

@ -1,41 +1,33 @@
import MountWidget from "discourse/components/mount-widget";
import { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
import discourseComputed from "discourse-common/utils/decorators";
export default MountWidget.extend({
widget: "avatar-flair",
export default Component.extend({
tagName: "",
@observes("user")
_rerender() {
this.queueRerender();
},
buildArgs() {
if (!this.user) {
@discourseComputed("user")
flair(user) {
if (!user) {
return;
}
if (
this.user.primary_group_flair_url ||
this.user.primary_group_flair_bg_color
) {
if (user.flair_url || user.flair_bg_color) {
return {
primary_group_flair_url: this.user.primary_group_flair_url,
primary_group_flair_bg_color: this.user.primary_group_flair_bg_color,
primary_group_flair_color: this.user.primary_group_flair_color,
primary_group_name: this.user.primary_group_name,
flairName: user.flair_name,
flairUrl: user.flair_url,
flairBgColor: user.flair_bg_color,
flairColor: user.flair_color,
};
}
const autoFlairAttrs = autoGroupFlairForUser(this.site, user);
if (autoFlairAttrs) {
return {
flairName: autoFlairAttrs.flair_name,
flairUrl: autoFlairAttrs.flair_url,
flairBgColor: autoFlairAttrs.flair_bg_color,
flairColor: autoFlairAttrs.flair_color,
};
} else {
const autoFlairAttrs = autoGroupFlairForUser(this.site, this.user);
if (autoFlairAttrs) {
return {
primary_group_flair_url: autoFlairAttrs.primary_group_flair_url,
primary_group_flair_bg_color:
autoFlairAttrs.primary_group_flair_bg_color,
primary_group_flair_color: autoFlairAttrs.primary_group_flair_color,
primary_group_name: autoFlairAttrs.primary_group_name,
};
}
}
},
});

View File

@ -1,7 +1,7 @@
import EmberObject, { action } from "@ember/object";
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Badge from "discourse/models/badge";
import EmberObject from "@ember/object";
import I18n from "I18n";
import UserBadge from "discourse/models/user-badge";
@ -50,35 +50,6 @@ export default Controller.extend({
return this.siteSettings.enable_badges && hasTitleBadges && hasBadge;
},
actions: {
loadMore() {
if (this.loadingMore) {
return;
}
this.set("loadingMore", true);
const userBadges = this.userBadges;
UserBadge.findByBadgeId(this.get("model.id"), {
offset: userBadges.length,
username: this.username,
})
.then((result) => {
userBadges.pushObjects(result);
if (userBadges.length === 0) {
this.set("noMoreBadges", true);
}
})
.finally(() => {
this.set("loadingMore", false);
});
},
toggleSetUserTitle() {
return this.toggleProperty("hiddenSetTitle");
},
},
@discourseComputed("noMoreBadges", "grantCount", "userBadges.length")
canLoadMore(noMoreBadges, grantCount, userBadgeLength) {
if (noMoreBadges) {
@ -96,4 +67,37 @@ export default Controller.extend({
_showFooter() {
this.set("application.showFooter", !this.canLoadMore);
},
@action
loadMore() {
if (!this.canLoadMore) {
return;
}
if (this.loadingMore) {
return;
}
this.set("loadingMore", true);
const userBadges = this.userBadges;
UserBadge.findByBadgeId(this.get("model.id"), {
offset: userBadges.length,
username: this.username,
})
.then((result) => {
userBadges.pushObjects(result);
if (userBadges.length === 0) {
this.set("noMoreBadges", true);
}
})
.finally(() => {
this.set("loadingMore", false);
});
},
@action
toggleSetUserTitle() {
return this.toggleProperty("hiddenSetTitle");
},
});

View File

@ -104,6 +104,7 @@ export default Controller.extend({
prioritizedCategoryId: null,
lastValidatedAt: null,
isUploading: false,
isProcessingUpload: false,
topic: null,
linkLookup: null,
showPreview: true,
@ -456,7 +457,7 @@ export default Controller.extend({
$links.each((idx, l) => {
const href = l.href;
if (href && href.length) {
// skip links in quotes
// skip links in quotes and oneboxes
for (let element = l; element; element = element.parentElement) {
if (
element.tagName === "DIV" &&
@ -471,6 +472,14 @@ export default Controller.extend({
) {
return true;
}
if (
element.tagName === "ASIDE" &&
element.classList.contains("onebox") &&
href !== element.dataset["onebox-src"]
) {
return true;
}
}
const [warn, info] = linkLookup.check(post, href);

View File

@ -99,10 +99,15 @@ export default Controller.extend(
return this.invite
.save(data)
.then(() => {
.then((result) => {
this.rollbackBuffer();
this.setAutogenerated(opts.autogenerated);
if (!this.autogenerated) {
if (result.warnings) {
this.appEvents.trigger("modal-body:flash", {
text: result.warnings.join(","),
messageClass: "warning",
});
} else if (!this.autogenerated) {
if (this.isEmail && opts.sendEmail) {
this.send("closeModal");
} else {

View File

@ -13,6 +13,7 @@ export const queryParams = {
before: { replace: true, refreshModel: true },
bumped_before: { replace: true, refreshModel: true },
f: { replace: true, refreshModel: true },
period: { replace: true, refreshModel: true },
};
// Basic controller options

View File

@ -37,9 +37,10 @@ export default Controller.extend({
}/l`;
}
url += "/top/" + period;
url += "/top";
const queryParams = this.router.currentRoute.queryParams;
let queryParams = this.router.currentRoute.queryParams;
queryParams.period = period;
if (Object.keys(queryParams).length) {
url =
`${url}?` +

View File

@ -1,12 +1,4 @@
import {
alias,
empty,
equal,
gt,
not,
notEmpty,
readOnly,
} from "@ember/object/computed";
import { alias, empty, equal, gt, not, readOnly } from "@ember/object/computed";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import DiscoveryController from "discourse/controllers/discovery";
import I18n from "I18n";
@ -140,7 +132,7 @@ const controllerOpts = {
allLoaded: empty("model.more_topics_url"),
latest: endWith("model.filter", "latest"),
new: endWith("model.filter", "new"),
top: notEmpty("period"),
top: endWith("model.filter", "top"),
yearly: equal("period", "yearly"),
quarterly: equal("period", "quarterly"),
monthly: equal("period", "monthly"),
@ -187,7 +179,7 @@ const controllerOpts = {
return I18n.t("topics.none.educate." + tab, {
userPrefsUrl: userPath(
`${this.currentUser.get("username_lower")}/preferences`
`${this.currentUser.get("username_lower")}/preferences/notifications`
),
});
},

View File

@ -20,53 +20,77 @@ export default Controller.extend(ModalFunctionality, {
loading: false,
isPublic: "true",
@discourseComputed("model.closed")
publicTimerTypes(closed) {
let types = [
{
id: CLOSE_STATUS_TYPE,
name: I18n.t(
closed ? "topic.temp_open.title" : "topic.auto_close.title"
),
},
];
@discourseComputed(
"model.closed",
"model.category",
"model.isPrivateMessage",
"model.invisible"
)
publicTimerTypes(closed, category, isPrivateMessage, invisible) {
let types = [];
if (!closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.auto_close.title"),
});
types.push({
id: CLOSE_AFTER_LAST_POST_STATUS_TYPE,
name: I18n.t("topic.auto_close_after_last_post.title"),
});
}
types.push(
{
if (closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t(
closed ? "topic.auto_reopen.title" : "topic.temp_close.title"
),
},
{
name: I18n.t("topic.auto_reopen.title"),
});
}
if (this.currentUser.staff) {
types.push({
id: DELETE_STATUS_TYPE,
name: I18n.t("topic.auto_delete.title"),
});
}
types.push({
id: BUMP_TYPE,
name: I18n.t("topic.auto_bump.title"),
});
if (this.currentUser.staff) {
types.push({
id: DELETE_REPLIES_TYPE,
name: I18n.t("topic.auto_delete_replies.title"),
});
}
if (closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.temp_open.title"),
});
}
if (!closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t("topic.temp_close.title"),
});
}
if (
(category && category.read_restricted) ||
isPrivateMessage ||
invisible
) {
types.push({
id: PUBLISH_TO_CATEGORY_STATUS_TYPE,
name: I18n.t("topic.publish_to_category.title"),
},
{
id: BUMP_TYPE,
name: I18n.t("topic.auto_bump.title"),
}
);
if (this.currentUser.get("staff")) {
types.push(
{
id: DELETE_STATUS_TYPE,
name: I18n.t("topic.auto_delete.title"),
},
{
id: DELETE_REPLIES_TYPE,
name: I18n.t("topic.auto_delete_replies.title"),
}
);
});
}
return types;
},

View File

@ -14,7 +14,7 @@ export default Controller.extend(ModalFunctionality, {
labelKey: null,
onShow() {
ajax("directory-columns.json")
ajax("edit-directory-columns.json")
.then((response) => {
this.setProperties({
loading: false,
@ -35,7 +35,7 @@ export default Controller.extend(ModalFunctionality, {
),
};
ajax("directory-columns.json", { type: "PUT", data })
ajax("edit-directory-columns.json", { type: "PUT", data })
.then(() => {
reload();
})
@ -58,7 +58,7 @@ export default Controller.extend(ModalFunctionality, {
.forEach((column, index) => {
column.setProperties({
position: column.automatic_position || index + 1,
enabled: column.automatic,
enabled: column.type === "automatic",
});
});
this.set("columns", resetColumns);

View File

@ -1,5 +1,6 @@
import { alias, equal, gte, none } from "@ember/object/computed";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import DiscourseURL from "discourse/lib/url";
import Controller from "@ember/controller";
import I18n from "I18n";
import { schedule } from "@ember/runloop";
@ -31,15 +32,15 @@ export default Controller.extend({
thrown: null,
lastTransition: null,
@discourseComputed
isNetwork() {
@discourseComputed("thrown")
isNetwork(thrown) {
// never made it on the wire
if (this.get("thrown.readyState") === 0) {
if (thrown && thrown.readyState === 0) {
return true;
}
// timed out
if (this.get("thrown.jqTextStatus") === "timeout") {
if (thrown && thrown.jqTextStatus === "timeout") {
return true;
}
@ -51,6 +52,9 @@ export default Controller.extend({
isServer: gte("thrown.status", 500),
isUnknown: none("isNetwork", "isServer"),
// Handling for the detailed_404 setting (which actually creates 403s)
errorHtml: alias("thrown.responseJSON.extras.html"),
// TODO
// make ajax requests to /srv/status with exponential backoff
// if one succeeds, set networkFixed to true, which puts a "Fixed!" message on the page
@ -62,16 +66,18 @@ export default Controller.extend({
this.set("loading", false);
},
@discourseComputed("isNetwork", "isServer", "isUnknown")
reason() {
if (this.isNetwork) {
@discourseComputed("isNetwork", "thrown.status", "thrown")
reason(isNetwork, thrownStatus, thrown) {
if (isNetwork) {
return I18n.t("errors.reasons.network");
} else if (this.isServer) {
} else if (thrownStatus >= 500) {
return I18n.t("errors.reasons.server");
} else if (this.isNotFound) {
} else if (thrownStatus === 404) {
return I18n.t("errors.reasons.not_found");
} else if (this.isForbidden) {
} else if (thrownStatus === 403) {
return I18n.t("errors.reasons.forbidden");
} else if (thrown === null) {
return I18n.t("errors.reasons.unknown");
} else {
// TODO
return I18n.t("errors.reasons.unknown");
@ -80,30 +86,42 @@ export default Controller.extend({
requestUrl: alias("thrown.requestedUrl"),
@discourseComputed("networkFixed", "isNetwork", "isServer", "isUnknown")
desc() {
if (this.networkFixed) {
@discourseComputed(
"networkFixed",
"isNetwork",
"thrown.status",
"thrown.statusText",
"thrown"
)
desc(networkFixed, isNetwork, thrownStatus, thrownStatusText, thrown) {
if (networkFixed) {
return I18n.t("errors.desc.network_fixed");
} else if (this.isNetwork) {
} else if (isNetwork) {
return I18n.t("errors.desc.network");
} else if (this.isNotFound) {
} else if (thrownStatus === 404) {
return I18n.t("errors.desc.not_found");
} else if (this.isServer) {
} else if (thrownStatus === 403) {
return I18n.t("errors.desc.forbidden");
} else if (thrownStatus >= 500) {
return I18n.t("errors.desc.server", {
status: this.get("thrown.status") + " " + this.get("thrown.statusText"),
status: thrownStatus + " " + thrownStatusText,
});
} else if (thrown === null) {
return I18n.t("errors.desc.unknown");
} else {
// TODO
return I18n.t("errors.desc.unknown");
}
},
@discourseComputed("networkFixed", "isNetwork", "isServer", "isUnknown")
enabledButtons() {
if (this.networkFixed) {
@discourseComputed("networkFixed", "isNetwork", "lastTransition")
enabledButtons(networkFixed, isNetwork, lastTransition) {
if (networkFixed) {
return [ButtonLoadPage];
} else if (this.isNetwork) {
} else if (isNetwork) {
return [ButtonBackDim, ButtonTryAgain];
} else if (!lastTransition) {
return [ButtonBackBright];
} else {
return [ButtonBackBright, ButtonTryAgain];
}
@ -111,14 +129,25 @@ export default Controller.extend({
actions: {
back() {
window.history.back();
// Strip off subfolder
const currentURL = DiscourseURL.router.location.getURL();
if (this.lastTransition && currentURL !== "/exception") {
this.lastTransition.abort();
this.setProperties({ lastTransition: null, thrown: null });
// Can't use routeTo because it handles navigation to the same page
DiscourseURL.handleURL(currentURL);
} else {
window.history.back();
}
},
tryLoading() {
this.set("loading", true);
schedule("afterRender", () => {
this.lastTransition.retry();
const transition = this.lastTransition;
this.setProperties({ lastTransition: null, thrown: null });
transition.retry();
this.set("loading", false);
});
},

View File

@ -1,3 +1,4 @@
import { schedule } from "@ember/runloop";
import ActionSummary from "discourse/models/action-summary";
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
@ -6,7 +7,7 @@ import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { Promise } from "rsvp";
import User from "discourse/models/user";
import discourseComputed from "discourse-common/utils/decorators";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { not } from "@ember/object/computed";
import optionalService from "discourse/lib/optional-service";
import { popupAjaxError } from "discourse/lib/ajax-error";
@ -52,6 +53,17 @@ export default Controller.extend(ModalFunctionality, {
};
},
@bind
keyDown(event) {
// CTRL+ENTER or CMD+ENTER
if (event.keyCode === 13 && (event.ctrlKey || event.metaKey)) {
if (this.submitEnabled) {
this.send("createFlag");
return false;
}
}
},
clientSuspend(performAction) {
this._penalize("showSuspendModal", performAction);
},
@ -85,6 +97,16 @@ export default Controller.extend(ModalFunctionality, {
this.set("spammerDetails", result);
});
}
schedule("afterRender", () => {
const element = document.querySelector(".flag-modal");
element.addEventListener("keydown", this.keyDown);
});
},
onClose() {
const element = document.querySelector(".flag-modal");
element.removeEventListener("keydown", this.keyDown);
},
@discourseComputed("spammerDetails.canDelete", "selected.name_key")

View File

@ -245,8 +245,8 @@ export default Controller.extend({
const searchKey = getSearchKey(args);
ajax("/search", { data: args })
.then((results) => {
const model = translateResults(results) || {};
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (results.grouped_search_result) {
this.set("q", results.grouped_search_result.term);

View File

@ -1,70 +1,65 @@
import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { emailValid } from "discourse/lib/utilities";
import { extractError } from "discourse/lib/ajax-error";
import { isEmpty } from "@ember/utils";
import { reads } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { emailValid } from "discourse/lib/utilities";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export default Controller.extend(ModalFunctionality, {
loading: false,
setAsOwner: false,
notifyUsers: false,
usernamesAndEmails: null,
emailsPresent: reads("emails.length"),
setOwner: false,
notifyUsers: false,
onShow() {
this.setProperties({
usernamesAndEmails: [],
setAsOwner: false,
loading: false,
setOwner: false,
notifyUsers: false,
usernamesAndEmails: [],
});
},
@discourseComputed("usernamesAndEmails", "loading")
disableAddButton(usernamesAndEmails, loading) {
return loading || !usernamesAndEmails || !(usernamesAndEmails.length > 0);
},
@discourseComputed("usernamesAndEmails")
notifyUsersDisabled() {
return this.usernames.length === 0 && this.emails.length > 0;
},
@discourseComputed("model.name", "model.full_name")
title(name, fullName) {
rawTitle(name, fullName) {
return I18n.t("groups.add_members.title", { group_name: fullName || name });
},
@discourseComputed("usernamesAndEmails.[]")
emails(usernamesAndEmails) {
return usernamesAndEmails.filter(emailValid).join(",");
},
@discourseComputed("usernamesAndEmails.[]")
usernames(usernamesAndEmails) {
return usernamesAndEmails.reject(emailValid).join(",");
},
@discourseComputed("usernamesAndEmails.[]")
emails(usernamesAndEmails) {
return usernamesAndEmails.filter(emailValid).join(",");
},
@action
setUsernamesAndEmails(usernamesAndEmails) {
this.set("usernamesAndEmails", usernamesAndEmails);
if (this.emails) {
if (!this.usernames) {
this.set("notifyUsers", false);
}
this.set("setOwner", false);
}
},
@action
addMembers() {
this.set("loading", true);
if (this.emailsPresent) {
this.set("setAsOwner", false);
}
if (this.notifyUsersDisabled) {
this.set("notifyUsers", false);
}
if (isEmpty(this.usernamesAndEmails)) {
return;
}
const promise = this.setAsOwner
this.set("loading", true);
const promise = this.setOwner
? this.model.addOwners(this.usernames, true, this.notifyUsers)
: this.model.addMembers(
this.usernames,
@ -75,14 +70,8 @@ export default Controller.extend(ModalFunctionality, {
promise
.then(() => {
let queryParams = {};
if (this.usernames) {
queryParams.filter = this.usernames;
}
this.transitionToRoute("group.members", this.get("model.name"), {
queryParams,
queryParams: this.usernames ? { filter: this.usernames } : {},
});
this.send("closeModal");

View File

@ -0,0 +1,5 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import ModalUpdateExistingUsers from "discourse/mixins/modal-update-existing-users";
export default Controller.extend(ModalFunctionality, ModalUpdateExistingUsers);

View File

@ -29,7 +29,7 @@ export default Controller.extend({
});
if (!automatic) {
if (this.siteSettings.enable_imap && this.siteSettings.enable_smtp) {
if (this.siteSettings.enable_smtp) {
defaultTabs.splice(2, 0, {
route: "group.manage.email",
title: "groups.manage.email.title",

View File

@ -101,21 +101,6 @@ export default Controller.extend({
return (fullName || displayName).capitalize();
},
@discourseComputed(
"model.name",
"model.flair_url",
"model.flair_bg_color",
"model.flair_color"
)
avatarFlairAttributes(groupName, flairURL, flairBgColor, flairColor) {
return {
primary_group_flair_url: flairURL,
primary_group_flair_bg_color: flairBgColor,
primary_group_flair_color: flairColor,
primary_group_name: groupName,
};
},
@discourseComputed("model.messageable")
displayGroupMessageButton(messageable) {
return this.currentUser && messageable;

View File

@ -28,6 +28,7 @@ export default Controller.extend(
invitedBy: readOnly("model.invited_by"),
email: alias("model.email"),
accountEmail: alias("email"),
hiddenEmail: alias("model.hidden_email"),
emailVerifiedByLink: alias("model.email_verified_by_link"),
accountUsername: alias("model.username"),

View File

@ -114,7 +114,7 @@ export default Controller.extend(ModalFunctionality, {
keysDelimiter: PLUS,
}),
help: buildShortcut("application.help", { keys1: ["?"] }),
dismiss_new_posts: buildShortcut("application.dismiss_new_posts", {
dismiss_new: buildShortcut("application.dismiss_new", {
keys1: ["x", "r"],
}),
dismiss_topics: buildShortcut("application.dismiss_topics", {
@ -151,10 +151,6 @@ export default Controller.extend(ModalFunctionality, {
keys1: ["n", "d"],
shortcutsDelimiter: "space",
}),
next_week: buildShortcut("bookmarks.next_week", {
keys1: ["n", "w"],
shortcutsDelimiter: "space",
}),
next_business_week: buildShortcut("bookmarks.next_business_week", {
keys1: ["n", "b", "w"],
shortcutsDelimiter: "space",

View File

@ -15,7 +15,12 @@ export default Controller.extend(CanCheckEmails, {
init() {
this._super(...arguments);
this.saveAttrNames = ["name", "title", "primary_group_id"];
this.saveAttrNames = [
"name",
"title",
"primary_group_id",
"flair_group_id",
];
this.set("revoking", {});
},
@ -45,6 +50,7 @@ export default Controller.extend(CanCheckEmails, {
},
canSelectTitle: gt("model.availableTitles.length", 0),
canSelectFlair: gt("model.availableFlairs.length", 0),
@discourseComputed("model.filteredGroups")
canSelectPrimaryGroup(primaryGroupOptions) {
@ -132,6 +138,7 @@ export default Controller.extend(CanCheckEmails, {
name: this.newNameInput,
title: this.newTitleInput,
primary_group_id: this.newPrimaryGroupInput,
flair_group_id: this.newFlairGroupId,
});
return this.model

View File

@ -1,4 +1,5 @@
import Controller, { inject as controller } from "@ember/controller";
import Session from "discourse/models/session";
import {
iOSWithVisualViewport,
isiPad,
@ -392,8 +393,10 @@ export default Controller.extend({
this.themeId,
true
);
Session.currentProp("darkModeAvailable", false);
} else {
loadColorSchemeStylesheet(colorSchemeId, this.themeId, true);
Session.currentProp("darkModeAvailable", true);
}
},

View File

@ -57,6 +57,10 @@ export default Controller.extend({
"model.can_upload_user_card_background"
),
experimentalUserCardImageUpload: readOnly(
"siteSettings.enable_experimental_image_uploader"
),
actions: {
showFeaturedTopicModal() {
showModal("feature-topic-on-profile", {
@ -86,21 +90,29 @@ export default Controller.extend({
this.model.set("user_option.timezone", moment.tz.guess());
},
save() {
this.set("saved", false);
_updateUserFields() {
const model = this.model,
userFields = this.userFields;
// Update the user fields
if (!isEmpty(userFields)) {
const modelFields = model.get("user_fields");
if (!isEmpty(modelFields)) {
userFields.forEach(function (uf) {
modelFields[uf.get("field.id").toString()] = uf.get("value");
const value = uf.get("value");
modelFields[uf.get("field.id").toString()] = isEmpty(value)
? null
: value;
});
}
}
},
save() {
this.set("saved", false);
const model = this.model;
// Update the user fields
this.send("_updateUserFields");
return model
.save(this.saveAttrNames)

View File

@ -60,56 +60,97 @@ export default Controller.extend(ModalFunctionality, Evented, {
this.notifyPropertyChange("categoriesBuffered");
},
countDescendants(category) {
return category.get("subcategories")
? category
.get("subcategories")
.reduce(
(count, subcategory) => count + this.countDescendants(subcategory),
category.get("subcategories").length
)
: 0;
},
move(category, direction) {
let otherCategory;
let targetPosition = category.get("position") + direction;
if (direction === -1) {
// First category above current one
const categoriesOrderedDesc = this.categoriesOrdered.reverse();
otherCategory = categoriesOrderedDesc.find(
(c) =>
category.get("parent_category_id") === c.get("parent_category_id") &&
c.get("position") < category.get("position")
);
} else if (direction === 1) {
// First category under current one
otherCategory = this.categoriesOrdered.find(
(c) =>
category.get("parent_category_id") === c.get("parent_category_id") &&
c.get("position") > category.get("position")
);
} else {
// Find category occupying target position
otherCategory = this.categoriesOrdered.find(
(c) => c.get("position") === category.get("position") + direction
);
}
if (otherCategory) {
// Try to swap positions of the two categories
if (category.get("id") !== otherCategory.get("id")) {
const currentPosition = category.get("position");
category.set("position", otherCategory.get("position"));
otherCategory.set("position", currentPosition);
// Adjust target position for sub-categories
if (direction > 0) {
// Moving down (position gets larger)
if (category.get("isParent")) {
// This category has subcategories, adjust targetPosition to account for them
let offset = this.countDescendants(category);
if (direction <= offset) {
// Only apply offset if target position is occupied by a subcategory
// Seems weird but fixes a UX quirk
targetPosition += offset;
}
}
} else {
// Moving up (position gets smaller)
const otherCategory = this.categoriesOrdered.find(
(c) =>
// find category currently at targetPosition
c.get("position") === targetPosition
);
if (otherCategory && otherCategory.get("ancestors")) {
// Target category is a subcategory, adjust targetPosition to account for ancestors
const highestAncestor = otherCategory
.get("ancestors")
.reduce((current, min) =>
current.get("position") < min.get("position") ? current : min
);
targetPosition = highestAncestor.get("position");
}
} else if (direction < 0) {
category.set("position", -1);
} else if (direction > 0) {
category.set("position", this.categoriesOrdered.length);
}
// Adjust target position for range bounds
if (targetPosition >= this.categoriesOrdered.length) {
// Set to max
targetPosition = this.categoriesOrdered.length - 1;
} else if (targetPosition < 0) {
// Set to min
targetPosition = 0;
}
// Update other categories between current and target position
this.categoriesOrdered.map((c) => {
if (direction < 0) {
// Moving up (position gets smaller)
if (
c.get("position") < category.get("position") &&
c.get("position") >= targetPosition
) {
const newPosition = c.get("position") + 1;
c.set("position", newPosition);
}
} else {
// Moving down (position gets larger)
if (
c.get("position") > category.get("position") &&
c.get("position") <= targetPosition
) {
const newPosition = c.get("position") - 1;
c.set("position", newPosition);
}
}
});
// Update this category's position to target position
category.set("position", targetPosition);
this.reorder();
},
actions: {
change(category, event) {
let newPosition = parseInt(event.target.value, 10);
newPosition = Math.min(
Math.max(newPosition, 0),
this.categoriesOrdered.length - 1
);
this.move(category, newPosition - category.get("position"));
let newPosition = parseFloat(event.target.value);
newPosition =
newPosition < category.get("position")
? Math.ceil(newPosition)
: Math.floor(newPosition);
const direction = newPosition - category.get("position");
this.move(category, direction);
},
moveUp(category) {

View File

@ -39,14 +39,6 @@ export default Controller.extend(
);
},
@action
copied() {
return this.appEvents.trigger("modal-body:flash", {
text: I18n.t("topic.share.copied"),
messageClass: "success",
});
},
@action
onChangeUsers(usernames) {
this.set("users", usernames.uniq());

View File

@ -8,6 +8,7 @@ import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { queryParams } from "discourse/controllers/discovery-sortable";
import { endWith } from "discourse/lib/computed";
export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
application: controller(),
@ -27,6 +28,8 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
max_posts: null,
q: null,
showInfo: false,
top: endWith("list.filter", "top"),
period: alias("list.for_period"),
@discourseComputed(
"canCreateTopic",
@ -131,8 +134,30 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
this.setProperties({ order, ascending: false });
}
let params = { order, ascending: this.ascending };
if (this.period) {
params.period = this.period;
}
this.transitionToRoute({
queryParams: { order, ascending: this.ascending },
queryParams: params,
});
},
changePeriod(p) {
this.set("period", p);
let params = { period: this.period };
if (!(this.order === "default" && this.ascending === false)) {
params = Object.assign(params, {
order: this.order,
ascending: this.ascending,
});
}
this.transitionToRoute({
queryParams: params,
});
},

View File

@ -6,7 +6,7 @@ import { isEmpty, isPresent } from "@ember/utils";
import { later, next, schedule } from "@ember/runloop";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import Composer from "discourse/models/composer";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
import I18n from "I18n";
import Post from "discourse/models/post";
import { Promise } from "rsvp";
@ -68,6 +68,8 @@ export default Controller.extend(bufferedProperty("model"), {
filter: null,
quoteState: null,
currentPostId: null,
userLastReadPostNumber: null,
highestPostNumber: null,
init() {
this._super(...arguments);
@ -949,10 +951,6 @@ export default Controller.extend(bufferedProperty("model"), {
});
},
recoverTopic() {
this.model.recover();
},
makeBanner() {
this.model.makeBanner();
},
@ -1204,83 +1202,89 @@ export default Controller.extend(bufferedProperty("model"), {
post.appEvents.trigger("post-stream:refresh", { id: post.id });
},
afterSave: (savedData) => {
this._addOrUpdateBookmarkedPost(post.id, savedData.reminderAt);
post.createBookmark(savedData);
resolve({ closedWithoutSaving: false });
},
afterDelete: (topicBookmarked) => {
this.model.set(
"bookmarked_posts",
this.model.bookmarked_posts.filter((x) => x.post_id !== post.id)
);
post.deleteBookmark(topicBookmarked);
},
});
});
},
_addOrUpdateBookmarkedPost(postId, reminderAt) {
if (!this.model.bookmarked_posts) {
this.model.set("bookmarked_posts", []);
}
let bookmarkedPost = this.model.bookmarked_posts.findBy("post_id", postId);
if (!bookmarkedPost) {
bookmarkedPost = { post_id: postId };
this.model.bookmarked_posts.pushObject(bookmarkedPost);
}
bookmarkedPost.reminder_at = reminderAt;
},
_toggleTopicBookmark() {
if (this.model.bookmarking) {
return Promise.resolve();
}
this.model.set("bookmarking", true);
const bookmark = !this.model.bookmarked;
let posts = this.model.postStream.posts;
const bookmarkedPostsCount = this.model.bookmarked_posts
? this.model.bookmarked_posts.length
: 0;
return this.model.firstPost().then((firstPost) => {
const toggleBookmarkOnServer = () => {
if (bookmark) {
return this._togglePostBookmark(firstPost).then((opts) => {
this.model.set("bookmarking", false);
if (opts && opts.closedWithoutSaving) {
return;
}
return this.model.afterTopicBookmarked(firstPost);
});
} else {
return this.model
.deleteBookmark()
.then(() => {
this.model.toggleProperty("bookmarked");
this.model.set("bookmark_reminder_at", null);
let clearedBookmarkProps = {
bookmarked: false,
bookmark_id: null,
bookmark_name: null,
bookmark_reminder_at: null,
};
if (posts) {
const updated = [];
posts.forEach((post) => {
if (post.bookmarked) {
post.setProperties(clearedBookmarkProps);
updated.push(post.id);
}
});
firstPost.setProperties(clearedBookmarkProps);
return updated;
}
})
.catch(popupAjaxError)
.finally(() => this.model.set("bookmarking", false));
}
};
const unbookmarkedPosts = [];
if (!bookmark && posts) {
posts.forEach(
(post) => post.bookmarked && unbookmarkedPosts.push(post)
);
const bookmarkPost = async (post) => {
const opts = await this._togglePostBookmark(post);
this.model.set("bookmarking", false);
if (opts.closedWithoutSaving) {
return;
}
this.model.afterPostBookmarked(post);
return [post.id];
};
return new Promise((resolve) => {
if (unbookmarkedPosts.length > 1) {
bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) =>
confirmed ? toggleBookmarkOnServer().then(resolve) : resolve()
);
} else {
toggleBookmarkOnServer().then(resolve);
}
});
const toggleBookmarkOnServer = async () => {
if (bookmarkedPostsCount === 0) {
const firstPost = await this.model.firstPost();
return bookmarkPost(firstPost);
} else if (bookmarkedPostsCount === 1) {
const postId = this.model.bookmarked_posts[0].post_id;
const post = await this.model.postById(postId);
return bookmarkPost(post);
} else {
return this.model
.deleteBookmarks()
.then(() => this.model.clearBookmarks())
.catch(popupAjaxError)
.finally(() => this.model.set("bookmarking", false));
}
};
return new Promise((resolve) => {
if (bookmarkedPostsCount > 1) {
bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
toggleBookmarkOnServer().then(resolve);
} else {
this.model.set("bookmarking", false);
resolve();
}
}
);
} else {
toggleBookmarkOnServer().then(resolve);
}
});
},
@ -1418,6 +1422,7 @@ export default Controller.extend(bufferedProperty("model"), {
return spinnerHTML;
},
@action
recoverTopic() {
this.model.recover();
},

View File

@ -1,69 +0,0 @@
import {
allowsAttachments,
authorizedExtensions,
uploadIcon,
} from "discourse/lib/uploads";
import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
export default Controller.extend(ModalFunctionality, {
imageUrl: null,
local: equal("selection", "local"),
remote: equal("selection", "remote"),
selection: "local",
@discourseComputed()
allowAdditionalFormats() {
return allowsAttachments(this.currentUser.staff, this.siteSettings);
},
@discourseComputed()
uploadIcon() {
return uploadIcon(this.currentUser.staff, this.siteSettings);
},
@discourseComputed("allowAdditionalFormats")
title(allowAdditionalFormats) {
const suffix = allowAdditionalFormats ? "_with_attachments" : "";
return `upload_selector.title${suffix}`;
},
@discourseComputed("selection", "allowAdditionalFormats")
tip(selection, allowAdditionalFormats) {
const suffix = allowAdditionalFormats ? "_with_attachments" : "";
return I18n.t(`upload_selector.${selection}_tip${suffix}`);
},
@discourseComputed()
supportedFormats() {
const extensions = authorizedExtensions(
this.currentUser.staff,
this.siteSettings
);
return `(${extensions})`;
},
actions: {
upload() {
if (this.local) {
$(".wmd-controls").fileupload("add", {
fileInput: $("#filename-input"),
});
} else {
const imageUrl = this.imageUrl || "";
const toolbarEvent = this.toolbarEvent;
if (imageUrl.match(/\.(jpg|jpeg|png|gif|heic|heif|webp)$/)) {
toolbarEvent.addText(`![](${imageUrl})`);
} else {
toolbarEvent.addText(imageUrl);
}
}
this.send("closeModal");
},
},
});

View File

@ -1,19 +1,18 @@
import Controller, { inject as controller } from "@ember/controller";
import { action, computed } from "@ember/object";
import { action } from "@ember/object";
import { alias, filterBy, sort } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
user: controller(),
username: alias("user.model.username_lower"),
sortedBadges: sort("model", "badgeSortOrder"),
favoriteBadges: filterBy("model", "is_favorite", true),
canFavoriteMoreBadges: computed(
"favoriteBadges.length",
"model.meta.max_favorites",
function () {
return this.favoriteBadges.length < this.model.meta.max_favorites;
}
),
@discourseComputed("favoriteBadges.length")
canFavoriteMoreBadges(favoriteBadgesCount) {
return favoriteBadgesCount < this.siteSettings.max_favorite_badges;
},
init() {
this._super(...arguments);

View File

@ -1,61 +1,28 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { alias, and, equal } from "@ember/object/computed";
import I18n from "I18n";
import Topic from "discourse/models/topic";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { VIEW_NAME_WARNINGS } from "discourse/routes/user-private-messages-warnings";
export default Controller.extend({
userTopicsList: controller("user-topics-list"),
user: controller(),
pmView: false,
viewingSelf: alias("user.viewingSelf"),
isGroup: equal("pmView", "groups"),
currentPath: alias("router._router.currentPath"),
selected: alias("userTopicsList.selected"),
bulkSelectEnabled: alias("userTopicsList.bulkSelectEnabled"),
showToggleBulkSelect: true,
pmTaggingEnabled: alias("site.can_tag_pms"),
tagId: null,
showNewPM: and("user.viewingSelf", "currentUser.can_send_private_messages"),
@discourseComputed("selected.[]", "bulkSelectEnabled")
hasSelection(selected, bulkSelectEnabled) {
return bulkSelectEnabled && selected && selected.length > 0;
},
bulkOperation(operation) {
const selected = this.selected;
let params = { type: operation };
if (this.isGroup) {
params.group = this.groupFilter;
}
Topic.bulkOperation(selected, params).then(
() => {
const model = this.get("userTopicsList.model");
const topics = model.get("topics");
topics.removeObjects(selected);
selected.clear();
model.loadMore();
},
() => {
bootbox.alert(I18n.t("user.messages.failed_to_move"));
}
);
@discourseComputed("viewingSelf", "pmView", "currentUser.admin")
showWarningsWarning(viewingSelf, pmView, isAdmin) {
return pmView === VIEW_NAME_WARNINGS && !viewingSelf && !isAdmin;
},
@action
changeGroupNotificationLevel(notificationLevel) {
this.group.setNotification(notificationLevel, this.get("user.model.id"));
},
@action
toggleBulkSelect() {
this.selected.clear();
this.toggleProperty("bulkSelectEnabled");
},
});

View File

@ -1,8 +1,9 @@
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
// Lists of topics on a user's page.
export default Controller.extend({
export default Controller.extend(BulkTopicSelection, {
application: controller(),
hideCategory: false,

View File

@ -1,4 +1,5 @@
import Controller, { inject as controller } from "@ember/controller";
import Group from "discourse/models/group";
import { action } from "@ember/object";
import discourseDebounce from "discourse-common/lib/debounce";
import showModal from "discourse/lib/show-modal";
@ -18,28 +19,45 @@ export default Controller.extend({
exclude_usernames: null,
isLoading: false,
columns: null,
groupsOptions: null,
params: null,
showTimeRead: equal("period", "all"),
loadUsers(params) {
this.set("isLoading", true);
loadUsers(params = null) {
if (params) {
this.set("params", params);
}
this.set("nameInput", params.name);
this.set("order", params.order);
this.setProperties({
isLoading: true,
nameInput: this.params.name,
order: this.params.order,
});
const custom_field_columns = this.columns.filter((c) => !c.automatic);
const user_field_ids = custom_field_columns
const userFieldIds = this.columns
.filter((c) => c.type === "user_field")
.map((c) => c.user_field_id)
.join("|");
const pluginColumnIds = this.columns
.filter((c) => c.type === "plugin")
.map((c) => c.id)
.join("|");
this.store
.find("directoryItem", Object.assign(params, { user_field_ids }))
return this.store
.find(
"directoryItem",
Object.assign(this.params, {
user_field_ids: userFieldIds,
plugin_column_ids: pluginColumnIds,
})
)
.then((model) => {
const lastUpdatedAt = model.get("resultSetMeta.last_updated_at");
this.setProperties({
model,
lastUpdatedAt: lastUpdatedAt ? longDate(lastUpdatedAt) : null,
period: params.period,
period: this.params.period,
});
})
.finally(() => {
@ -47,18 +65,40 @@ export default Controller.extend({
});
},
loadGroups() {
return Group.findAll({ ignore_automatic: true }).then((groups) => {
const groupOptions = groups.map((group) => {
return {
name: group.full_name || group.name,
id: group.name,
};
});
this.set("groupOptions", groupOptions);
});
},
@action
groupChanged(_, groupAttrs) {
// First param is the group name, which include none or 'all groups'. Ignore this and look at second param.
this.set("group", groupAttrs.id);
},
@action
showEditColumnsModal() {
showModal("edit-user-directory-columns");
},
@action
onFilterChanged(filter) {
discourseDebounce(this, this._setName, filter, 500);
onUsernameFilterChanged(filter) {
discourseDebounce(this, this._setUsernameFilter, filter, 500);
},
_setName(name) {
this.set("name", name);
_setUsernameFilter(username) {
this.setProperties({
name: username,
"params.name": username,
});
this.loadUsers();
},
@observes("model.canLoadMore")

View File

@ -0,0 +1,39 @@
import { htmlSafe } from "@ember/template";
import { number } from "discourse/lib/formatter";
import { registerUnbound } from "discourse-common/lib/helpers";
import I18n from "I18n";
registerUnbound("mobile-directory-item-label", function (args) {
// Args should include key/values { item, column }
const count = args.item.get(args.column.name);
const translationPrefix =
args.column.type === "automatic" ? "directory." : "";
return htmlSafe(I18n.t(`${translationPrefix}${args.column.name}`, { count }));
});
registerUnbound("directory-item-value", function (args) {
// Args should include key/values { item, column }
return htmlSafe(
`<span class='number'>${number(args.item.get(args.column.name))}</span>`
);
});
registerUnbound("directory-item-user-field-value", function (args) {
// Args should include key/values { item, column }
const value =
args.item.user && args.item.user.user_fields
? args.item.user.user_fields[args.column.user_field_id]
: null;
const content = value || "-";
return htmlSafe(`<span class='user-field-value'>${content}</span>`);
});
registerUnbound("directory-column-is-automatic", function (args) {
// Args should include key/values { column }
return args.column.type === "automatic";
});
registerUnbound("directory-column-is-user-field", function (args) {
// Args should include key/values { column }
return args.column.type === "user_field";
});

View File

@ -1,10 +0,0 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
import I18n from "I18n";
export default registerUnbound("mobile-directory-item-label", function (args) {
// Args should include key/values { item, column }
const count = args.item.get(args.column.name);
return htmlSafe(I18n.t(`directory.${args.column.name}`, { count }));
});

View File

@ -1,16 +0,0 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
export default registerUnbound(
"directory-item-user-field-value",
function (args) {
// Args should include key/values { item, column }
const value =
args.item.user && args.item.user.user_fields
? args.item.user.user_fields[args.column.user_field_id]
: null;
const content = value || "-";
return htmlSafe(`<span class='user-field-value'>${content}</span>`);
}
);

View File

@ -1,11 +0,0 @@
import { htmlSafe } from "@ember/template";
import { registerUnbound } from "discourse-common/lib/helpers";
import { number } from "discourse/lib/formatter";
export default registerUnbound("directory-item-value", function (args) {
// Args should include key/values { item, column }
return htmlSafe(
`<span class='number'>${number(args.item.get(args.column.name))}</span>`
);
});

View File

@ -1,13 +1,9 @@
const {
A: emberArray,
Helper,
assert,
computed,
get,
getOwner,
run,
runInDebug,
} = Ember;
import { A } from "@ember/array";
import Helper from "@ember/component/helper";
import { computed, get } from "@ember/object";
import { getOwner } from "@ember/application";
import { run } from "@ember/runloop";
import { assert, runInDebug } from "@ember/debug";
function getCurrentRouteInfos(router) {
let routerLib = router._routerMicrolib || router.router;
@ -15,12 +11,12 @@ function getCurrentRouteInfos(router) {
}
function getRoutes(router) {
return emberArray(getCurrentRouteInfos(router)).mapBy("_route").reverse();
return A(getCurrentRouteInfos(router)).mapBy("_route").reverse();
}
function getRouteWithAction(router, actionName) {
let action;
let handler = emberArray(getRoutes(router)).find((route) => {
let handler = A(getRoutes(router)).find((route) => {
let actions = route.actions || route._actions;
action = actions[actionName];

View File

@ -16,6 +16,8 @@ registerUnbound("topic-link", (topic, args) => {
return htmlSafe(
`<a href='${url}'
role='heading'
level='2'
class='${classes.join(" ")}'
data-topic-id='${topic.id}'>${title}</a>`
);

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