Version bump
This commit is contained in:
commit
a615eecd36
22
.github/workflows/tests.yml
vendored
22
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
6
Gemfile
6
Gemfile
@ -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
|
||||
|
||||
55
Gemfile.lock
55
Gemfile.lock
@ -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
|
||||
|
||||
@ -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 "";
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -89,6 +89,7 @@ export default Component.extend({
|
||||
word: "",
|
||||
replacement: "",
|
||||
formSubmitted: false,
|
||||
selectedTags: [],
|
||||
showMessage: true,
|
||||
message: I18n.t("admin.watched_words.form.success"),
|
||||
});
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -35,7 +35,6 @@ export default Controller.extend({
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.set("model", model);
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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")) || [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 }),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -9,4 +9,9 @@ export default Route.extend({
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.resetState();
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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" });
|
||||
|
||||
@ -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();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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)}}
|
||||
→ <span class="replacement">{{word.replacement}}</span>
|
||||
{{else if isTag}}
|
||||
|
||||
@ -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"
|
||||
}}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
everyTag=true
|
||||
options=(hash
|
||||
allowAny=false
|
||||
maximum=null
|
||||
)
|
||||
}}
|
||||
<div class="desc">{{html-safe setting.description}}</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if user.canSuspend}}
|
||||
{{admin-penalty-history user=user}}
|
||||
|
||||
<div class="until-controls">
|
||||
<label>
|
||||
{{future-date-input
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -34,6 +34,8 @@ var define, requirejs;
|
||||
inject: Ember.inject.controller,
|
||||
},
|
||||
"@ember/debug": {
|
||||
assert: Ember.assert,
|
||||
runInDebug: Ember.runInDebug,
|
||||
warn: Ember.warn,
|
||||
},
|
||||
"@ember/object": {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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) {}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
@ -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")
|
||||
|
||||
@ -49,7 +49,9 @@ export default MountWidget.extend({
|
||||
"selectedPostsCount",
|
||||
"searchService",
|
||||
"showReadIndicator",
|
||||
"streamFilters"
|
||||
"streamFilters",
|
||||
"lastReadPostNumber",
|
||||
"highestPostNumber"
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
classNameBindings: [":social-link"],
|
||||
|
||||
tagName: "",
|
||||
actions: {
|
||||
share: function (source) {
|
||||
this.action(source);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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"),
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}?` +
|
||||
|
||||
@ -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`
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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(``);
|
||||
} else {
|
||||
toolbarEvent.addText(imageUrl);
|
||||
}
|
||||
}
|
||||
this.send("closeModal");
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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";
|
||||
});
|
||||
@ -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 }));
|
||||
});
|
||||
@ -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>`);
|
||||
}
|
||||
);
|
||||
@ -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>`
|
||||
);
|
||||
});
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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
Reference in New Issue
Block a user