Version bump
This commit is contained in:
commit
1a46b092fc
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@ -232,15 +232,15 @@ jobs:
|
||||
|
||||
- name: Core QUnit 1
|
||||
working-directory: ./app/assets/javascripts/discourse
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}"
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" --random
|
||||
timeout-minutes: 20
|
||||
|
||||
- name: Core QUnit 2
|
||||
working-directory: ./app/assets/javascripts/discourse
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}"
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" --random
|
||||
timeout-minutes: 20
|
||||
|
||||
- name: Core QUnit 3
|
||||
working-directory: ./app/assets/javascripts/discourse
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}"
|
||||
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" --random
|
||||
timeout-minutes: 20
|
||||
|
||||
12
Gemfile
12
Gemfile
@ -105,9 +105,7 @@ gem 'omniauth-oauth2', require: false
|
||||
|
||||
gem 'omniauth-google-oauth2'
|
||||
|
||||
# Pinning oj until https://github.com/ohler55/oj/issues/699 is resolved.
|
||||
# Segfaults and stuck processes after upgrading.
|
||||
gem 'oj', '3.13.2'
|
||||
gem 'oj'
|
||||
|
||||
gem 'pg'
|
||||
gem 'mini_sql'
|
||||
@ -135,6 +133,14 @@ gem 'cose', require: false
|
||||
gem 'addressable'
|
||||
gem 'json_schemer'
|
||||
|
||||
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1")
|
||||
# net-smtp, net-imap and net-pop were removed from default gems in Ruby 3.1
|
||||
gem "net-smtp", "~> 0.2.1", require: false
|
||||
gem "net-imap", "~> 0.2.1", require: false
|
||||
gem "net-pop", "~> 0.1.1", require: false
|
||||
gem "digest", "3.0.0", require: false
|
||||
end
|
||||
|
||||
# Gems used only for assets and not required in production environments by default.
|
||||
# Allow everywhere for now cause we are allowing asset debugging in production
|
||||
group :assets do
|
||||
|
||||
90
Gemfile.lock
90
Gemfile.lock
@ -48,8 +48,8 @@ GEM
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.2.0)
|
||||
@ -80,8 +80,8 @@ GEM
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.9.4)
|
||||
msgpack (~> 1.0)
|
||||
bootsnap (1.10.3)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
@ -97,7 +97,7 @@ GEM
|
||||
cose (1.2.0)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
cppjieba_rb (0.3.3)
|
||||
cppjieba_rb (0.4.2)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
@ -129,10 +129,10 @@ GEM
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.10.0)
|
||||
excon (0.89.0)
|
||||
excon (0.91.0)
|
||||
execjs (2.8.1)
|
||||
exifr (1.3.9)
|
||||
fabrication (2.24.0)
|
||||
fabrication (2.27.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
fakeweb (1.3.0)
|
||||
@ -175,7 +175,7 @@ GEM
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http_accept_language (2.1.1)
|
||||
i18n (1.8.11)
|
||||
i18n (1.9.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_optim (0.31.1)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
@ -184,8 +184,8 @@ GEM
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (3.0.1)
|
||||
in_threads (1.5.4)
|
||||
ipaddr (1.2.3)
|
||||
in_threads (1.6.0)
|
||||
ipaddr (1.2.4)
|
||||
jmespath (1.5.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
@ -231,32 +231,34 @@ GEM
|
||||
rack (>= 1.1.3)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.6.1)
|
||||
mini_racer (0.6.1)
|
||||
mini_portile2 (2.7.1)
|
||||
mini_racer (0.6.2)
|
||||
libv8-node (~> 16.10.0.0)
|
||||
mini_scheduler (0.13.0)
|
||||
sidekiq (>= 4.2.3)
|
||||
mini_sql (1.1.3)
|
||||
mini_sql (1.3.0)
|
||||
mini_suffix (0.3.3)
|
||||
ffi (~> 1.9)
|
||||
minitest (5.15.0)
|
||||
mocha (1.13.0)
|
||||
mock_redis (0.29.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.4.2)
|
||||
msgpack (1.4.4)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
mustache (1.1.1)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.12.5)
|
||||
mini_portile2 (~> 2.6.1)
|
||||
nokogiri (1.13.1)
|
||||
mini_portile2 (~> 2.7.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.5-arm64-darwin)
|
||||
nokogiri (1.13.1-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.5-x86_64-darwin)
|
||||
nokogiri (1.13.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.12.5-x86_64-linux)
|
||||
nokogiri (1.13.1-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.1-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.8)
|
||||
oauth2 (1.4.7)
|
||||
@ -265,7 +267,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.13.2)
|
||||
oj (3.13.11)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -298,7 +300,7 @@ GEM
|
||||
parallel
|
||||
parser (3.1.0.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
pg (1.3.1)
|
||||
progress (3.6.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
@ -309,7 +311,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
puma (5.6.1)
|
||||
nio4r (~> 2.0)
|
||||
r2 (0.2.7)
|
||||
racc (1.6.0)
|
||||
@ -341,7 +343,7 @@ GEM
|
||||
rainbow (3.1.1)
|
||||
raindrops (0.20.0)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-fsevent (0.11.1)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rbtrace (0.4.14)
|
||||
@ -353,7 +355,7 @@ GEM
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.2.0)
|
||||
request_store (1.5.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
rexml (3.2.5)
|
||||
rinku (2.0.6)
|
||||
@ -362,22 +364,22 @@ GEM
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.2)
|
||||
rspec (3.11.0)
|
||||
rspec-core (~> 3.11.0)
|
||||
rspec-expectations (~> 3.11.0)
|
||||
rspec-mocks (~> 3.11.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-html-matchers (0.9.4)
|
||||
nokogiri (~> 1)
|
||||
rspec (>= 3.0.0.a, < 4)
|
||||
rspec-mocks (3.10.2)
|
||||
rspec-mocks (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-rails (5.1.0)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
@ -385,15 +387,15 @@ GEM
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.3)
|
||||
rspec-support (3.11.0)
|
||||
rss (0.2.9)
|
||||
rexml
|
||||
rswag-specs (2.4.0)
|
||||
activesupport (>= 3.1, < 7.0)
|
||||
rswag-specs (2.5.1)
|
||||
activesupport (>= 3.1, < 7.1)
|
||||
json-schema (~> 2.2)
|
||||
railties (>= 3.1, < 7.0)
|
||||
railties (>= 3.1, < 7.1)
|
||||
rtlit (0.0.5)
|
||||
rubocop (1.25.0)
|
||||
rubocop (1.25.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
@ -407,7 +409,7 @@ GEM
|
||||
rubocop-discourse (2.5.0)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.7.0)
|
||||
rubocop-rspec (2.8.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-prof (1.4.3)
|
||||
ruby-progressbar (1.11.0)
|
||||
@ -433,7 +435,7 @@ GEM
|
||||
activesupport (>= 3.1)
|
||||
shoulda-matchers (5.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.3.1)
|
||||
sidekiq (6.4.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@ -477,7 +479,7 @@ GEM
|
||||
jwt (~> 2.0)
|
||||
xorcist (1.1.2)
|
||||
yaml-lint (0.0.10)
|
||||
zeitwerk (2.5.3)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@ -558,7 +560,7 @@ DEPENDENCIES
|
||||
multi_json
|
||||
mustache
|
||||
nokogiri
|
||||
oj (= 3.13.2)
|
||||
oj
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
omniauth-github
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { action, computed } from "@ember/object";
|
||||
import loadScript, { loadCSS } from "discourse/lib/load-script";
|
||||
import Component from "@ember/component";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { schedule } from "@ember/runloop";
|
||||
|
||||
/**
|
||||
An input field for a color.
|
||||
@ -22,13 +20,25 @@ export default Component.extend({
|
||||
return this.onlyHex ? 6 : null;
|
||||
}),
|
||||
|
||||
normalizedHexValue: computed("hexValue", function () {
|
||||
return this.normalize(this.hexValue);
|
||||
}),
|
||||
|
||||
normalize(color) {
|
||||
if (/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
|
||||
if (this._valid(color)) {
|
||||
if (!color.startsWith("#")) {
|
||||
color = "#" + color;
|
||||
}
|
||||
if (color.length === 4) {
|
||||
color =
|
||||
"#" +
|
||||
color
|
||||
.slice(1)
|
||||
.split("")
|
||||
.map((hex) => hex + hex)
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
},
|
||||
|
||||
@ -39,49 +49,25 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
onPickerInput(event) {
|
||||
this.set("hexValue", event.target.value.replace("#", ""));
|
||||
},
|
||||
|
||||
@observes("hexValue", "brightnessValue", "valid")
|
||||
hexValueChanged() {
|
||||
const hex = this.hexValue;
|
||||
let text = this.element.querySelector("input.hex-input");
|
||||
|
||||
if (this.attrs.onChangeColor) {
|
||||
this.attrs.onChangeColor(this.normalize(hex));
|
||||
}
|
||||
|
||||
if (this.valid) {
|
||||
this.styleSelection &&
|
||||
text.setAttribute(
|
||||
"style",
|
||||
"color: " +
|
||||
(this.brightnessValue > 125 ? "black" : "white") +
|
||||
"; background-color: #" +
|
||||
hex +
|
||||
";"
|
||||
);
|
||||
|
||||
if (this.pickerLoaded) {
|
||||
$(this.element.querySelector(".picker")).spectrum({
|
||||
color: "#" + hex,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.styleSelection && text.setAttribute("style", "");
|
||||
if (this._valid()) {
|
||||
this.element.querySelector(".picker").value = this.normalize(hex);
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
loadScript("/javascripts/spectrum.js").then(() => {
|
||||
loadCSS("/javascripts/spectrum.css").then(() => {
|
||||
schedule("afterRender", () => {
|
||||
$(this.element.querySelector(".picker"))
|
||||
.spectrum({ color: "#" + this.hexValue })
|
||||
.on("change.spectrum", (me, color) => {
|
||||
this.set("hexValue", color.toHexString().replace("#", ""));
|
||||
});
|
||||
this.set("pickerLoaded", true);
|
||||
});
|
||||
});
|
||||
});
|
||||
schedule("afterRender", () => this.hexValueChanged());
|
||||
_valid(color = this.hexValue) {
|
||||
return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { empty } from "@ember/object/computed";
|
||||
import { on } from "discourse-common/utils/decorators";
|
||||
import discourseComputed, { on } from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: [":simple-list", ":value-list"],
|
||||
@ -47,10 +47,32 @@ export default Component.extend({
|
||||
this._onChange();
|
||||
},
|
||||
|
||||
@action
|
||||
shift(operation, index) {
|
||||
let futureIndex = index + operation;
|
||||
|
||||
if (futureIndex > this.collection.length - 1) {
|
||||
futureIndex = 0;
|
||||
} else if (futureIndex < 0) {
|
||||
futureIndex = this.collection.length - 1;
|
||||
}
|
||||
|
||||
const shiftedValue = this.collection[index];
|
||||
this.collection.removeAt(index);
|
||||
this.collection.insertAt(futureIndex, shiftedValue);
|
||||
|
||||
this._onChange();
|
||||
},
|
||||
|
||||
_onChange() {
|
||||
this.attrs.onChange && this.attrs.onChange(this.collection);
|
||||
},
|
||||
|
||||
@discourseComputed("collection")
|
||||
showUpDownButtons(collection) {
|
||||
return collection.length - 1 ? true : false;
|
||||
},
|
||||
|
||||
_splitValues(values, delimiter) {
|
||||
return values && values.length
|
||||
? values.split(delimiter || "\n").filter(Boolean)
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import Component from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
|
||||
export default Component.extend({
|
||||
tokenSeparator: "|",
|
||||
choices: null,
|
||||
|
||||
@computed("value")
|
||||
get settingValue() {
|
||||
return this.value.toString().split(this.tokenSeparator).filter(Boolean);
|
||||
},
|
||||
|
||||
@action
|
||||
onChange(value) {
|
||||
if (value.some((v) => v.includes("?") || v.includes("*"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("value", value.join(this.tokenSeparator));
|
||||
},
|
||||
});
|
||||
@ -59,6 +59,22 @@ export default Component.extend({
|
||||
selectChoice(choice) {
|
||||
this._addValue(choice);
|
||||
},
|
||||
|
||||
shift(operation, index) {
|
||||
let futureIndex = index + operation;
|
||||
|
||||
if (futureIndex > this.collection.length - 1) {
|
||||
futureIndex = 0;
|
||||
} else if (futureIndex < 0) {
|
||||
futureIndex = this.collection.length - 1;
|
||||
}
|
||||
|
||||
const shiftedValue = this.collection[index];
|
||||
this.collection.removeAt(index);
|
||||
this.collection.insertAt(futureIndex, shiftedValue);
|
||||
|
||||
this._saveValues();
|
||||
},
|
||||
},
|
||||
|
||||
_addValue(value) {
|
||||
@ -99,6 +115,11 @@ export default Component.extend({
|
||||
this.set("values", this.collection.join(this.inputDelimiter || "\n"));
|
||||
},
|
||||
|
||||
@discourseComputed("collection")
|
||||
showUpDownButtons(collection) {
|
||||
return collection.length - 1 ? true : false;
|
||||
},
|
||||
|
||||
_splitValues(values, delimiter) {
|
||||
if (values && values.length) {
|
||||
return values.split(delimiter).filter((x) => x);
|
||||
|
||||
@ -5,6 +5,7 @@ import Permalink from "admin/models/permalink";
|
||||
import bootbox from "bootbox";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
|
||||
export default Controller.extend({
|
||||
loading: false,
|
||||
@ -29,12 +30,7 @@ export default Controller.extend({
|
||||
|
||||
copyUrl(pl) {
|
||||
let linkElement = document.querySelector(`#admin-permalink-${pl.id}`);
|
||||
let textArea = document.createElement("textarea");
|
||||
textArea.value = linkElement.textContent;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("Copy");
|
||||
textArea.remove();
|
||||
clipboardCopy(linkElement.textContent);
|
||||
},
|
||||
|
||||
destroy(record) {
|
||||
|
||||
@ -163,9 +163,23 @@ const Report = EmberObject.extend({
|
||||
return this._computeTrend(prev, total, higherIsBetter);
|
||||
},
|
||||
|
||||
@discourseComputed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
|
||||
thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
|
||||
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
||||
@discourseComputed(
|
||||
"prev30Days",
|
||||
"prev_period",
|
||||
"lastThirtyDaysCount",
|
||||
"higher_is_better"
|
||||
)
|
||||
thirtyDaysTrend(
|
||||
prev30Days,
|
||||
prev_period,
|
||||
lastThirtyDaysCount,
|
||||
higherIsBetter
|
||||
) {
|
||||
return this._computeTrend(
|
||||
prev30Days ?? prev_period,
|
||||
lastThirtyDaysCount,
|
||||
higherIsBetter
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("type")
|
||||
@ -236,10 +250,15 @@ const Report = EmberObject.extend({
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("prev30Days", "lastThirtyDaysCount")
|
||||
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
|
||||
@discourseComputed("prev30Days", "prev_period")
|
||||
canDisplayTrendIcon(prev30Days, prev_period) {
|
||||
return prev30Days ?? prev_period;
|
||||
},
|
||||
|
||||
@discourseComputed("prev30Days", "prev_period", "lastThirtyDaysCount")
|
||||
thirtyDaysCountTitle(prev30Days, prev_period, lastThirtyDaysCount) {
|
||||
return this.changeTitle(
|
||||
prev30Days,
|
||||
prev30Days ?? prev_period,
|
||||
lastThirtyDaysCount,
|
||||
"in the previous 30 day period"
|
||||
);
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item route="adminSiteSettings" label="admin.site_settings.title"}}
|
||||
{{/if}}
|
||||
{{nav-item route="adminUsersList" label="admin.users.title"}}
|
||||
{{nav-item route="adminUsers" label="admin.users.title"}}
|
||||
{{#if showGroups}}
|
||||
{{nav-item route="groups" label="admin.groups.title"}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class="field">{{i18n name}}</div>
|
||||
<div class="value">
|
||||
{{#if editing}}
|
||||
{{text-field value=buffer autofocus="autofocus" autocomplete="discourse"}}
|
||||
{{text-field value=buffer autofocus="autofocus" autocomplete="off"}}
|
||||
{{else}}
|
||||
<a href {{action "edit"}} class="inline-editable-field">
|
||||
<span>{{value}}</span>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<div class="cell value thirty-days-count {{model.thirtyDaysTrend}}" title={{model.thirtyDaysCountTitle}}>
|
||||
{{number model.lastThirtyDaysCount}}
|
||||
|
||||
{{#if model.prev30Days}}
|
||||
{{#if model.canDisplayTrendIcon}}
|
||||
{{d-icon model.thirtyDaysTrendIcon}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -15,8 +15,23 @@
|
||||
class="value-input"
|
||||
focus-out=(action "changeValue" index)
|
||||
}}
|
||||
|
||||
{{#if showUpDownButtons}}
|
||||
{{d-button
|
||||
action=(action "shift" -1 index)
|
||||
icon="arrow-up"
|
||||
class="shift-up-value-btn btn-small"
|
||||
}}
|
||||
{{d-button
|
||||
action=(action "shift" 1 index)
|
||||
icon="arrow-down"
|
||||
class="shift-down-value-btn btn-small"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -26,9 +41,10 @@
|
||||
value=newValue
|
||||
placeholderKey="admin.site_settings.simple_list.add_item"
|
||||
class="add-value-input"
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"}}
|
||||
autocapitalize="off"
|
||||
}}
|
||||
|
||||
{{d-button
|
||||
action=(action "addValue")
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
{{value-list values=value addKey="admin.site_settings.add_host"}}
|
||||
{{list-setting
|
||||
value=settingValue
|
||||
settingName=setting.setting
|
||||
choices=settingValue
|
||||
onChange=(action "onChange")
|
||||
options=(hash
|
||||
allowAny=allowAny
|
||||
)
|
||||
}}
|
||||
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
<div class="desc">{{html-safe setting.description}}</div>
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
{{input
|
||||
class="filter-input"
|
||||
placeholder=(i18n "admin.customize.theme.filter_placeholder")
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
type="search"
|
||||
value=(mut filterTerm)
|
||||
}}
|
||||
{{d-icon "search"}}
|
||||
|
||||
@ -15,6 +15,19 @@
|
||||
class="value-input"
|
||||
focus-out=(action "changeValue" index)
|
||||
}}
|
||||
|
||||
{{#if showUpDownButtons}}
|
||||
{{d-button
|
||||
action=(action "shift" -1 index)
|
||||
icon="arrow-up"
|
||||
class="shift-up-value-btn btn-small"
|
||||
}}
|
||||
{{d-button
|
||||
action=(action "shift" 1 index)
|
||||
icon="arrow-down"
|
||||
class="shift-down-value-btn btn-small"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
let cdn, baseUrl, baseUri, baseUriMatcher;
|
||||
let S3BaseUrl, S3CDN;
|
||||
|
||||
let snapshot;
|
||||
|
||||
export default function getURL(url) {
|
||||
if (baseUri === undefined) {
|
||||
setPrefix($('meta[name="discourse-base-uri"]').attr("content") || "");
|
||||
@ -59,15 +61,43 @@ export function setPrefix(configBaseUri) {
|
||||
baseUriMatcher = new RegExp(`^${baseUri}(/|$)`);
|
||||
}
|
||||
|
||||
export function setupURL(configCdn, configBaseUrl, configBaseUri) {
|
||||
export function setupURL(configCdn, configBaseUrl, configBaseUri, opts) {
|
||||
opts = opts || {};
|
||||
cdn = configCdn;
|
||||
baseUrl = configBaseUrl;
|
||||
setPrefix(configBaseUri);
|
||||
|
||||
if (opts?.snapshot) {
|
||||
snapshot = {
|
||||
cdn,
|
||||
baseUri,
|
||||
baseUrl,
|
||||
configBaseUrl,
|
||||
baseUriMatcher,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function setupS3CDN(configS3BaseUrl, configS3CDN) {
|
||||
// In a test environment we might change these values and, after tests, want to restore them.
|
||||
export function restoreBaseUri() {
|
||||
if (snapshot) {
|
||||
cdn = snapshot.cdn;
|
||||
baseUri = snapshot.baseUri;
|
||||
baseUrl = snapshot.baseUrl;
|
||||
baseUriMatcher = snapshot.baseUriMatcher;
|
||||
S3BaseUrl = snapshot.S3BaseUrl;
|
||||
S3CDN = snapshot.S3CDN;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupS3CDN(configS3BaseUrl, configS3CDN, opts) {
|
||||
S3BaseUrl = configS3BaseUrl;
|
||||
S3CDN = configS3CDN;
|
||||
if (opts?.snapshot) {
|
||||
snapshot = snapshot || {};
|
||||
snapshot.S3BaseUrl = S3BaseUrl;
|
||||
snapshot.S3CDN = S3CDN;
|
||||
}
|
||||
}
|
||||
|
||||
// We can use this to identify when navigating on the same host but outside of the
|
||||
|
||||
@ -1,12 +1,4 @@
|
||||
import {
|
||||
LATER_TODAY_CUTOFF_HOUR,
|
||||
MOMENT_THURSDAY,
|
||||
laterToday,
|
||||
now,
|
||||
parseCustomDatetime,
|
||||
startOfDay,
|
||||
tomorrow,
|
||||
} from "discourse/lib/time-utils";
|
||||
import { now, parseCustomDatetime, startOfDay } from "discourse/lib/time-utils";
|
||||
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
@ -305,9 +297,7 @@ export default Component.extend({
|
||||
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
|
||||
label: "time_shortcut.post_local_date",
|
||||
time: this._postLocalDate(),
|
||||
timeFormatted: this._postLocalDate().format(
|
||||
I18n.t("dates.long_no_year")
|
||||
),
|
||||
timeFormatKey: "dates.long_no_year",
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
@ -330,38 +320,13 @@ export default Component.extend({
|
||||
editingExistingBookmark,
|
||||
existingBookmarkHasReminder
|
||||
) {
|
||||
if (!editingExistingBookmark) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!existingBookmarkHasReminder) {
|
||||
if (editingExistingBookmark && !existingBookmarkHasReminder) {
|
||||
return [TIME_SHORTCUT_TYPES.NONE];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
additionalTimeShortcutOptions() {
|
||||
let additional = [];
|
||||
|
||||
if (
|
||||
!laterToday(this.userTimezone).isSame(
|
||||
tomorrow(this.userTimezone),
|
||||
"date"
|
||||
) &&
|
||||
now(this.userTimezone).hour() < LATER_TODAY_CUTOFF_HOUR
|
||||
) {
|
||||
additional.push(TIME_SHORTCUT_TYPES.LATER_TODAY);
|
||||
}
|
||||
|
||||
if (now(this.userTimezone).day() < MOMENT_THURSDAY) {
|
||||
additional.push(TIME_SHORTCUT_TYPES.LATER_THIS_WEEK);
|
||||
}
|
||||
|
||||
return additional;
|
||||
},
|
||||
|
||||
@discourseComputed("model.reminderAt")
|
||||
existingReminderAtFormatted(existingReminderAt) {
|
||||
return formattedReminderTime(existingReminderAt, this.userTimezone);
|
||||
|
||||
@ -100,6 +100,7 @@ export function cleanUpComposerUploadMarkdownResolver() {
|
||||
export default Component.extend(ComposerUploadUppy, {
|
||||
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
|
||||
|
||||
editorClass: ".d-editor",
|
||||
fileUploadElementId: "file-uploader",
|
||||
mobileFileUploaderId: "mobile-file-upload",
|
||||
eventPrefix: "composer",
|
||||
@ -199,7 +200,10 @@ export default Component.extend(ComposerUploadUppy, {
|
||||
|
||||
@discourseComputed()
|
||||
acceptsAllFormats() {
|
||||
return authorizesAllExtensions(this.currentUser.staff, this.siteSettings);
|
||||
return (
|
||||
this.capabilities.isIOS ||
|
||||
authorizesAllExtensions(this.currentUser.staff, this.siteSettings)
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
|
||||
@ -477,7 +477,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
key: "#",
|
||||
afterComplete: (value) => {
|
||||
this.set("value", value);
|
||||
return this._focusTextArea();
|
||||
schedule("afterRender", this, this._focusTextArea);
|
||||
},
|
||||
transformComplete: (obj) => {
|
||||
return obj.text;
|
||||
@ -504,7 +504,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
key: ":",
|
||||
afterComplete: (text) => {
|
||||
this.set("value", text);
|
||||
this._focusTextArea();
|
||||
schedule("afterRender", this, this._focusTextArea);
|
||||
},
|
||||
|
||||
onKeyUp: (text, cp) => {
|
||||
@ -821,7 +821,6 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
applyList: (head, exampleKey, opts) =>
|
||||
this._applyList(selected, head, exampleKey, opts),
|
||||
addText: (text) => this._addText(selected, text),
|
||||
replaceText: (text) => this._addText({ pre: "", post: "" }, text),
|
||||
getText: () => this.value,
|
||||
toggleDirection: () => this._toggleDirection(),
|
||||
};
|
||||
|
||||
@ -14,13 +14,9 @@ import I18n from "I18n";
|
||||
import { action } from "@ember/object";
|
||||
import Component from "@ember/component";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import {
|
||||
MOMENT_MONDAY,
|
||||
now,
|
||||
startOfDay,
|
||||
thisWeekend,
|
||||
} from "discourse/lib/time-utils";
|
||||
import { MOMENT_MONDAY, now, startOfDay } from "discourse/lib/time-utils";
|
||||
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
|
||||
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
|
||||
import ItsATrap from "@discourse/itsatrap";
|
||||
|
||||
export default Component.extend({
|
||||
@ -86,26 +82,20 @@ export default Component.extend({
|
||||
|
||||
@discourseComputed()
|
||||
customTimeShortcutOptions() {
|
||||
const timezone = this.currentUser.resolvedTimezone(this.currentUser);
|
||||
return [
|
||||
{
|
||||
icon: "bed",
|
||||
id: "this_weekend",
|
||||
label: "time_shortcut.this_weekend",
|
||||
time: thisWeekend(),
|
||||
timeFormatKey: "dates.time_short_day",
|
||||
},
|
||||
{
|
||||
icon: "far-clock",
|
||||
id: "two_weeks",
|
||||
label: "time_shortcut.two_weeks",
|
||||
time: startOfDay(now().add(2, "weeks").day(MOMENT_MONDAY)),
|
||||
time: startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY)),
|
||||
timeFormatKey: "dates.long_no_year",
|
||||
},
|
||||
{
|
||||
icon: "far-calendar-plus",
|
||||
id: "six_months",
|
||||
label: "time_shortcut.six_months",
|
||||
time: startOfDay(now().add(6, "months").startOf("month")),
|
||||
time: startOfDay(now(timezone).add(6, "months").startOf("month")),
|
||||
timeFormatKey: "dates.long_no_year",
|
||||
},
|
||||
];
|
||||
@ -113,7 +103,11 @@ export default Component.extend({
|
||||
|
||||
@discourseComputed
|
||||
hiddenTimeShortcutOptions() {
|
||||
return ["none"];
|
||||
return [
|
||||
TIME_SHORTCUT_TYPES.NONE,
|
||||
TIME_SHORTCUT_TYPES.LATER_TODAY,
|
||||
TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,
|
||||
];
|
||||
},
|
||||
|
||||
isCustom: equal("timerType", "custom"),
|
||||
|
||||
@ -79,7 +79,6 @@ export default Component.extend({
|
||||
now,
|
||||
day: now.day(),
|
||||
includeWeekend: this.includeWeekend,
|
||||
includeMidFuture: this.includeMidFuture || true,
|
||||
includeFarFuture: this.includeFarFuture,
|
||||
includeDateTime: this.includeDateTime,
|
||||
canScheduleNow: this.includeNow || false,
|
||||
|
||||
@ -37,6 +37,7 @@ export default Component.extend({
|
||||
EmberObject.create({
|
||||
email_username: this.group.email_username,
|
||||
email_password: this.group.email_password,
|
||||
email_from_alias: this.group.email_from_alias,
|
||||
smtp_server: this.group.smtp_server,
|
||||
smtp_port: (this.group.smtp_port || "").toString(),
|
||||
smtp_ssl: this.group.smtp_ssl,
|
||||
@ -73,6 +74,7 @@ export default Component.extend({
|
||||
smtp_port: this.form.smtp_port,
|
||||
smtp_ssl: this.form.smtp_ssl,
|
||||
email_username: this.form.email_username,
|
||||
email_from_alias: this.form.email_from_alias,
|
||||
email_password: this.form.email_password,
|
||||
});
|
||||
})
|
||||
|
||||
@ -5,7 +5,7 @@ import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
export default Component.extend({
|
||||
classNameBindings: [":popup-tip", "good", "bad", "lastShownAt::hide"],
|
||||
attributeBindings: ["role"],
|
||||
attributeBindings: ["role", "ariaLabel"],
|
||||
rerenderTriggers: ["validation.reason"],
|
||||
tipReason: null,
|
||||
lastShownAt: or("shownAt", "validation.lastShownAt"),
|
||||
@ -19,6 +19,11 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("validation.reason")
|
||||
ariaLabel(reason) {
|
||||
return reason?.replace(/(<([^>]+)>)/gi, "");
|
||||
},
|
||||
|
||||
click() {
|
||||
this.set("shownAt", null);
|
||||
const composer = getOwner(this).lookup("controller:composer");
|
||||
|
||||
@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import {
|
||||
postUrl,
|
||||
selectedElement,
|
||||
selectedRange,
|
||||
selectedText,
|
||||
setCaretPosition,
|
||||
translateModKey,
|
||||
@ -164,10 +165,14 @@ export default Component.extend(KeyEnterEscape, {
|
||||
const cooked =
|
||||
$selectedElement.find(".cooked")[0] ||
|
||||
$selectedElement.closest(".cooked")[0];
|
||||
const postBody = toMarkdown(cooked.innerHTML);
|
||||
|
||||
// computing markdown takes a lot of time on long posts
|
||||
// this code attempts to compute it only when we can't fast track
|
||||
let opts = {
|
||||
full: _selectedText === postBody,
|
||||
full:
|
||||
selectedRange().startOffset > 0
|
||||
? false
|
||||
: _selectedText === toMarkdown(cooked.innerHTML),
|
||||
};
|
||||
|
||||
for (
|
||||
@ -192,22 +197,24 @@ export default Component.extend(KeyEnterEscape, {
|
||||
this.topic.postStream.findLoadedPost(postId)?.can_edit
|
||||
);
|
||||
|
||||
const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi");
|
||||
const matches = postBody.match(regexp);
|
||||
if (this._canEditPost) {
|
||||
const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi");
|
||||
const matches = cooked.innerHTML.match(regexp);
|
||||
|
||||
if (
|
||||
quoteState.buffer.length < 1 ||
|
||||
quoteState.buffer.includes("|") || // tables are too complex
|
||||
quoteState.buffer.match(/\n/g) || // linebreaks are too complex
|
||||
matches?.length > 1 // duplicates are too complex
|
||||
) {
|
||||
this.set("_isFastEditable", false);
|
||||
this.set("_fastEditInitalSelection", null);
|
||||
this.set("_fastEditNewSelection", null);
|
||||
} else if (matches?.length === 1) {
|
||||
this.set("_isFastEditable", true);
|
||||
this.set("_fastEditInitalSelection", quoteState.buffer);
|
||||
this.set("_fastEditNewSelection", quoteState.buffer);
|
||||
if (
|
||||
quoteState.buffer.length < 1 ||
|
||||
quoteState.buffer.includes("|") || // tables are too complex
|
||||
quoteState.buffer.match(/\n/g) || // linebreaks are too complex
|
||||
matches?.length > 1 // duplicates are too complex
|
||||
) {
|
||||
this.set("_isFastEditable", false);
|
||||
this.set("_fastEditInitalSelection", null);
|
||||
this.set("_fastEditNewSelection", null);
|
||||
} else if (matches?.length === 1) {
|
||||
this.set("_isFastEditable", true);
|
||||
this.set("_fastEditInitalSelection", quoteState.buffer);
|
||||
this.set("_fastEditNewSelection", quoteState.buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import TextField from "discourse/components/text-field";
|
||||
import { applySearchAutocomplete } from "discourse/lib/search";
|
||||
|
||||
export default TextField.extend({
|
||||
autocomplete: "discourse-search",
|
||||
autocomplete: "off",
|
||||
|
||||
@discourseComputed("searchService.searchContextEnabled")
|
||||
placeholder(searchContextEnabled) {
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import {
|
||||
LATER_TODAY_CUTOFF_HOUR,
|
||||
MOMENT_FRIDAY,
|
||||
MOMENT_THURSDAY,
|
||||
START_OF_DAY_HOUR,
|
||||
laterToday,
|
||||
now,
|
||||
@ -57,7 +60,6 @@ export default Component.extend({
|
||||
selectedDatetime: null,
|
||||
prefilledDatetime: null,
|
||||
|
||||
additionalOptionsToShow: null,
|
||||
hiddenOptions: null,
|
||||
customOptions: null,
|
||||
|
||||
@ -76,7 +78,6 @@ export default Component.extend({
|
||||
this.setProperties({
|
||||
customTime: this.defaultCustomReminderTime,
|
||||
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
|
||||
additionalOptionsToShow: this.additionalOptionsToShow || [],
|
||||
hiddenOptions: this.hiddenOptions || [],
|
||||
customOptions: this.customOptions || [],
|
||||
customLabels: this.customLabels || {},
|
||||
@ -168,38 +169,18 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"additionalOptionsToShow",
|
||||
"hiddenOptions",
|
||||
"customOptions",
|
||||
"customLabels",
|
||||
"userTimezone"
|
||||
)
|
||||
options(
|
||||
additionalOptionsToShow,
|
||||
hiddenOptions,
|
||||
customOptions,
|
||||
customLabels,
|
||||
userTimezone
|
||||
) {
|
||||
options(hiddenOptions, customOptions, customLabels, userTimezone) {
|
||||
this._loadLastUsedCustomDatetime();
|
||||
|
||||
let options = defaultShortcutOptions(userTimezone);
|
||||
|
||||
if (additionalOptionsToShow.length > 0) {
|
||||
options.forEach((opt) => {
|
||||
if (additionalOptionsToShow.includes(opt.id)) {
|
||||
opt.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
customOptions.forEach((opt) => {
|
||||
if (!opt.timeFormatted && opt.time) {
|
||||
opt.timeFormatted = opt.time.format(I18n.t(opt.timeFormatKey));
|
||||
}
|
||||
});
|
||||
|
||||
this._hideDynamicOptions(options);
|
||||
options = options.concat(customOptions);
|
||||
|
||||
options.sort((a, b) => {
|
||||
if (a.time < b.time) {
|
||||
return -1;
|
||||
@ -218,9 +199,7 @@ export default Component.extend({
|
||||
TIME_SHORTCUT_TYPES.LAST_CUSTOM
|
||||
);
|
||||
lastCustom.time = this.parsedLastCustomDatetime;
|
||||
lastCustom.timeFormatted = this.parsedLastCustomDatetime.format(
|
||||
I18n.t("dates.long_no_year")
|
||||
);
|
||||
lastCustom.timeFormatKey = "dates.long_no_year";
|
||||
lastCustom.hidden = false;
|
||||
}
|
||||
|
||||
@ -234,12 +213,8 @@ export default Component.extend({
|
||||
});
|
||||
}
|
||||
|
||||
options.forEach((option) => {
|
||||
if (customLabels[option.id]) {
|
||||
option.label = customLabels[option.id];
|
||||
}
|
||||
});
|
||||
|
||||
this._applyCustomLabels(options, customLabels);
|
||||
this._formatTime(options);
|
||||
return options;
|
||||
},
|
||||
|
||||
@ -288,4 +263,39 @@ export default Component.extend({
|
||||
this.onTimeSelected(type, dateTime);
|
||||
}
|
||||
},
|
||||
|
||||
_applyCustomLabels(options, customLabels) {
|
||||
options.forEach((option) => {
|
||||
if (customLabels[option.id]) {
|
||||
option.label = customLabels[option.id];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_formatTime(options) {
|
||||
options.forEach((option) => {
|
||||
if (option.time && option.timeFormatKey) {
|
||||
option.timeFormatted = option.time.format(I18n.t(option.timeFormatKey));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_hideDynamicOptions(options) {
|
||||
if (now(this.userTimezone).hour() >= LATER_TODAY_CUTOFF_HOUR) {
|
||||
this._hideOption(options, TIME_SHORTCUT_TYPES.LATER_TODAY);
|
||||
}
|
||||
|
||||
if (now(this.userTimezone).day() >= MOMENT_THURSDAY) {
|
||||
this._hideOption(options, TIME_SHORTCUT_TYPES.LATER_THIS_WEEK);
|
||||
}
|
||||
|
||||
if (now(this.userTimezone).day() >= MOMENT_FRIDAY) {
|
||||
this._hideOption(options, TIME_SHORTCUT_TYPES.THIS_WEEKEND);
|
||||
}
|
||||
},
|
||||
|
||||
_hideOption(options, optionId) {
|
||||
const option = options.findBy("id", optionId);
|
||||
option.hidden = true;
|
||||
},
|
||||
});
|
||||
|
||||
@ -204,25 +204,44 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
const topic = this.topic;
|
||||
const target = $(e.target);
|
||||
if (target.hasClass("bulk-select")) {
|
||||
if (e.target.classList.contains("bulk-select")) {
|
||||
const selected = this.selected;
|
||||
|
||||
if (target.is(":checked")) {
|
||||
if (e.target.checked) {
|
||||
selected.addObject(topic);
|
||||
|
||||
if (this.lastChecked && e.shiftKey) {
|
||||
const bulkSelects = Array.from(
|
||||
document.querySelectorAll("input.bulk-select")
|
||||
),
|
||||
from = bulkSelects.indexOf(e.target),
|
||||
to = bulkSelects.findIndex((el) => el.id === this.lastChecked.id),
|
||||
start = Math.min(from, to),
|
||||
end = Math.max(from, to);
|
||||
|
||||
bulkSelects
|
||||
.slice(start, end)
|
||||
.filter((el) => el.checked !== true)
|
||||
.forEach((checkbox) => {
|
||||
checkbox.click();
|
||||
});
|
||||
}
|
||||
|
||||
this.set("lastChecked", e.target);
|
||||
} else {
|
||||
selected.removeObject(topic);
|
||||
this.set("lastChecked", null);
|
||||
}
|
||||
}
|
||||
|
||||
if (target.hasClass("raw-topic-link")) {
|
||||
if (e.target.classList.contains("raw-topic-link")) {
|
||||
if (wantsNewWindow(e)) {
|
||||
return true;
|
||||
}
|
||||
return this.navigateToTopic(topic, target.attr("href"));
|
||||
return this.navigateToTopic(topic, e.target.getAttribute("href"));
|
||||
}
|
||||
|
||||
if (target.closest("a.topic-status").length === 1) {
|
||||
if (e.target.closest("a.topic-status")) {
|
||||
this.topic.togglePinnedForUser();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -163,6 +163,10 @@ export default Component.extend(PanEvents, {
|
||||
},
|
||||
|
||||
panStart(e) {
|
||||
if (e.originalEvent.target.classList.contains("docked")) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.originalEvent.preventDefault();
|
||||
const center = e.center;
|
||||
const $centeredElement = $(document.elementFromPoint(center.x, center.y));
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
authorizesOneOrMoreExtensions,
|
||||
uploadIcon,
|
||||
} from "discourse/lib/uploads";
|
||||
import { cancel, run } from "@ember/runloop";
|
||||
import { cancel, run, scheduleOnce } from "@ember/runloop";
|
||||
import {
|
||||
cannotPostAgain,
|
||||
durationTextFromSeconds,
|
||||
@ -396,6 +396,75 @@ export default Controller.extend({
|
||||
return uploadIcon(this.currentUser.staff, this.siteSettings);
|
||||
},
|
||||
|
||||
// Use this to open the composer when you are not sure whether it is
|
||||
// already open and whether it already has a draft being worked on. Supports
|
||||
// options to append text once the composer is open if required.
|
||||
//
|
||||
// opts:
|
||||
//
|
||||
// - topic: if this is present, the composer will be opened with the reply
|
||||
// action and the current topic key and draft sequence
|
||||
// - fallbackToNewTopic: if true, and there is no draft and no topic,
|
||||
// the composer will be opened with the create_topic action and a new
|
||||
// topic draft key
|
||||
// - insertText: the text to append to the composer once it is opened
|
||||
// - openOpts: this object will be passed to this.open if fallbackToNewTopic is
|
||||
// true or topic is provided
|
||||
@action
|
||||
focusComposer(opts = {}) {
|
||||
this._openComposerForFocus(opts).then(() => {
|
||||
this._focusAndInsertText(opts.insertText);
|
||||
});
|
||||
},
|
||||
|
||||
_openComposerForFocus(opts) {
|
||||
if (this.get("model.viewOpen")) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const opened = this.openIfDraft();
|
||||
if (opened) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (opts.topic) {
|
||||
return this.open(
|
||||
Object.assign(
|
||||
{
|
||||
action: Composer.REPLY,
|
||||
draftKey: opts.topic.get("draft_key"),
|
||||
draftSequence: opts.topic.get("draft_sequence"),
|
||||
topic: opts.topic,
|
||||
},
|
||||
opts.openOpts || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.fallbackToNewTopic) {
|
||||
return this.open(
|
||||
Object.assign(
|
||||
{
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: Composer.NEW_TOPIC_KEY,
|
||||
},
|
||||
opts.openOpts || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_focusAndInsertText(insertText) {
|
||||
scheduleOnce("afterRender", () => {
|
||||
const input = document.querySelector("textarea.d-editor-input");
|
||||
input && input.focus();
|
||||
|
||||
if (insertText) {
|
||||
this.model.appendText(insertText, null, { new_line: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
openIfDraft(event) {
|
||||
if (this.get("model.viewDraft")) {
|
||||
@ -407,7 +476,10 @@ export default Controller.extend({
|
||||
}
|
||||
|
||||
this.set("model.composeState", Composer.OPEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@ -100,9 +100,19 @@ export default Controller.extend(
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed("externalAuthsOnly", "discourseConnectEnabled")
|
||||
showSocialLoginAvailable(externalAuthsOnly, discourseConnectEnabled) {
|
||||
return !externalAuthsOnly && !discourseConnectEnabled;
|
||||
@discourseComputed(
|
||||
"externalAuthsEnabled",
|
||||
"externalAuthsOnly",
|
||||
"discourseConnectEnabled"
|
||||
)
|
||||
showSocialLoginAvailable(
|
||||
externalAuthsEnabled,
|
||||
externalAuthsOnly,
|
||||
discourseConnectEnabled
|
||||
) {
|
||||
return (
|
||||
externalAuthsEnabled && !externalAuthsOnly && !discourseConnectEnabled
|
||||
);
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
|
||||
@ -169,7 +169,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
DiscourseURL.routeTo(result.url);
|
||||
})
|
||||
.catch((xhr) => {
|
||||
this.flash(extractError(xhr, I18n.t("topic.move_to.error")));
|
||||
this.flash(extractError(xhr, I18n.t("topic.move_to.error")), "error");
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("saving", false);
|
||||
|
||||
@ -127,6 +127,13 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
|
||||
);
|
||||
},
|
||||
|
||||
showInserted() {
|
||||
const tracker = this.topicTrackingState;
|
||||
this.list.loadBefore(tracker.get("newIncoming"), true);
|
||||
tracker.resetTracking();
|
||||
return false;
|
||||
},
|
||||
|
||||
changeSort(order) {
|
||||
if (order === this.order) {
|
||||
this.toggleProperty("ascending");
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import { htmlHelper } from "discourse-common/lib/helpers";
|
||||
|
||||
export default htmlHelper((color) => `border-color: #${color}`);
|
||||
export default htmlHelper((color) => `border-color: #${color}; `);
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { htmlHelper } from "discourse-common/lib/helpers";
|
||||
|
||||
export default htmlHelper((color) => `--category-color: #${color};`);
|
||||
@ -36,5 +36,6 @@ export function autoLoadModules(container, registry) {
|
||||
|
||||
export default {
|
||||
name: "auto-load-modules",
|
||||
after: "inject-objects",
|
||||
initialize: (container) => autoLoadModules(container, container.registry),
|
||||
};
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { scheduleOnce } from "@ember/runloop";
|
||||
|
||||
function _clean(transition) {
|
||||
if (window.MiniProfiler && transition.from) {
|
||||
window.MiniProfiler.pageTransition();
|
||||
}
|
||||
|
||||
// Close some elements that may be open
|
||||
document.querySelectorAll("header ul.icons li").forEach((element) => {
|
||||
element.classList.remove("active");
|
||||
});
|
||||
|
||||
document.querySelectorAll(`[data-toggle="dropdown"]`).forEach((element) => {
|
||||
element.parentElement.classList.remove("open");
|
||||
});
|
||||
|
||||
// Close the lightbox
|
||||
if ($.magnificPopup?.instance) {
|
||||
$.magnificPopup.instance.close();
|
||||
document.body.classList.remove("mfp-zoom-out-cur");
|
||||
}
|
||||
|
||||
// Remove any link focus
|
||||
const { activeElement } = document;
|
||||
if (activeElement && !activeElement.classList.contains("no-blur")) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
this.lookup("route:application").send("closeModal");
|
||||
|
||||
this.lookup("service:app-events").trigger("dom:clean");
|
||||
this.lookup("service:document-title").updateContextCount(0);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "clean-dom-on-route-change",
|
||||
after: "inject-objects",
|
||||
|
||||
initialize(container) {
|
||||
const router = container.lookup("router:main");
|
||||
|
||||
router.on("routeDidChange", (transition) => {
|
||||
scheduleOnce("afterRender", container, _clean, transition);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -1,61 +1,10 @@
|
||||
import { cancel, later } from "@ember/runloop";
|
||||
import I18n from "I18n";
|
||||
import { Promise } from "rsvp";
|
||||
import { guidFor } from "@ember/object/internals";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
|
||||
// http://github.com/feross/clipboard-copy
|
||||
function clipboardCopy(text) {
|
||||
// Use the Async Clipboard API when available.
|
||||
// Requires a secure browsing context (i.e. HTTPS)
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text).catch(function (err) {
|
||||
throw err !== undefined
|
||||
? err
|
||||
: new DOMException("The request is not allowed", "NotAllowedError");
|
||||
});
|
||||
}
|
||||
|
||||
// ...Otherwise, use document.execCommand() fallback
|
||||
|
||||
// Put the text to copy into a <span>
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
|
||||
// Preserve consecutive spaces and newlines
|
||||
span.style.whiteSpace = "pre";
|
||||
|
||||
// Add the <span> to the page
|
||||
document.body.appendChild(span);
|
||||
|
||||
// Make a selection object representing the range of text selected by the user
|
||||
const selection = window.getSelection();
|
||||
const range = window.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
// Copy text to the clipboard
|
||||
let success = false;
|
||||
try {
|
||||
success = window.document.execCommand("copy");
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("error", err);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
selection.removeAllRanges();
|
||||
window.document.body.removeChild(span);
|
||||
|
||||
return success
|
||||
? Promise.resolve()
|
||||
: Promise.reject(
|
||||
new DOMException("The request is not allowed", "NotAllowedError")
|
||||
);
|
||||
}
|
||||
|
||||
let _copyCodeblocksClickHandlers = {};
|
||||
let _fadeCopyCodeblocksRunners = {};
|
||||
|
||||
@ -79,6 +28,25 @@ export default {
|
||||
_fadeCopyCodeblocksRunners = {};
|
||||
}
|
||||
|
||||
function _copyComplete(button) {
|
||||
button.classList.add("copied");
|
||||
const state = button.innerHTML;
|
||||
button.innerHTML = I18n.t("copy_codeblock.copied");
|
||||
|
||||
const commandId = guidFor(button);
|
||||
|
||||
if (_fadeCopyCodeblocksRunners[commandId]) {
|
||||
cancel(_fadeCopyCodeblocksRunners[commandId]);
|
||||
delete _fadeCopyCodeblocksRunners[commandId];
|
||||
}
|
||||
|
||||
_fadeCopyCodeblocksRunners[commandId] = later(() => {
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = state;
|
||||
delete _fadeCopyCodeblocksRunners[commandId];
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function _handleClick(event) {
|
||||
if (!event.target.classList.contains("copy-cmd")) {
|
||||
return;
|
||||
@ -96,24 +64,14 @@ export default {
|
||||
)
|
||||
.trim();
|
||||
|
||||
clipboardCopy(text).then(() => {
|
||||
button.classList.add("copied");
|
||||
const state = button.innerHTML;
|
||||
button.innerHTML = I18n.t("copy_codeblock.copied");
|
||||
|
||||
const commandId = guidFor(button);
|
||||
|
||||
if (_fadeCopyCodeblocksRunners[commandId]) {
|
||||
cancel(_fadeCopyCodeblocksRunners[commandId]);
|
||||
delete _fadeCopyCodeblocksRunners[commandId];
|
||||
}
|
||||
|
||||
_fadeCopyCodeblocksRunners[commandId] = later(() => {
|
||||
button.classList.remove("copied");
|
||||
button.innerHTML = state;
|
||||
delete _fadeCopyCodeblocksRunners[commandId];
|
||||
}, 3000);
|
||||
});
|
||||
const result = clipboardCopy(text);
|
||||
if (result.then) {
|
||||
result.then(() => {
|
||||
_copyComplete(button);
|
||||
});
|
||||
} else if (result) {
|
||||
_copyComplete(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,54 @@
|
||||
import { setDefaultOwner } from "discourse-common/lib/get-owner";
|
||||
import { isLegacyEmber } from "discourse-common/config/environment";
|
||||
import User from "discourse/models/user";
|
||||
import Site from "discourse/models/site";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
export default {
|
||||
name: "inject-objects",
|
||||
after: isLegacyEmber() ? null : "export-application-global",
|
||||
initialize(container, app) {
|
||||
// This is required for Ember CLI tests to work
|
||||
setDefaultOwner(app.__container__);
|
||||
|
||||
// Backwards compatibility for Discourse.SiteSettings and Discourse.User
|
||||
if (!isLegacyEmber()) {
|
||||
Object.defineProperty(app, "SiteSettings", {
|
||||
get() {
|
||||
deprecated(
|
||||
`use injected siteSettings instead of Discourse.SiteSettings`,
|
||||
{
|
||||
since: "2.8",
|
||||
dropFrom: "2.9",
|
||||
}
|
||||
);
|
||||
return container.lookup("site-settings:main");
|
||||
},
|
||||
});
|
||||
Object.defineProperty(app, "User", {
|
||||
get() {
|
||||
deprecated(
|
||||
`import discourse/models/user instead of using Discourse.User`,
|
||||
{
|
||||
since: "2.8",
|
||||
dropFrom: "2.9",
|
||||
}
|
||||
);
|
||||
return User;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(app, "Site", {
|
||||
get() {
|
||||
deprecated(
|
||||
`import discourse/models/site instead of using Discourse.Site`,
|
||||
{
|
||||
since: "2.8",
|
||||
dropFrom: "2.9",
|
||||
}
|
||||
);
|
||||
return Site;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
resetPageTracking,
|
||||
startPageTracking,
|
||||
} from "discourse/lib/page-tracker";
|
||||
import { cleanDOM } from "discourse/lib/clean-dom";
|
||||
import { viewTrackingRequired } from "discourse/lib/ajax";
|
||||
|
||||
export default {
|
||||
@ -13,11 +12,7 @@ export default {
|
||||
initialize(container) {
|
||||
// Tell our AJAX system to track a page transition
|
||||
const router = container.lookup("router:main");
|
||||
|
||||
router.on("routeWillChange", viewTrackingRequired);
|
||||
router.on("routeDidChange", (transition) => {
|
||||
cleanDOM(container, { skipMiniProfilerPageTransition: !transition.from });
|
||||
});
|
||||
|
||||
let appEvents = container.lookup("service:app-events");
|
||||
let documentTitle = container.lookup("service:document-title");
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import { scheduleOnce } from "@ember/runloop";
|
||||
|
||||
function _clean(opts = {}) {
|
||||
if (window.MiniProfiler && !opts.skipMiniProfilerPageTransition) {
|
||||
window.MiniProfiler.pageTransition();
|
||||
}
|
||||
|
||||
// Close some elements that may be open
|
||||
$("header ul.icons li").removeClass("active");
|
||||
$('[data-toggle="dropdown"]').parent().removeClass("open");
|
||||
// close the lightbox
|
||||
if ($.magnificPopup && $.magnificPopup.instance) {
|
||||
$.magnificPopup.instance.close();
|
||||
$("body").removeClass("mfp-zoom-out-cur");
|
||||
}
|
||||
|
||||
// Remove any link focus
|
||||
// NOTE: the '.not("body")' is here to prevent a bug in IE10 on Win7
|
||||
// cf. https://stackoverflow.com/questions/5657371
|
||||
$(document.activeElement).not("body").not(".no-blur").blur();
|
||||
|
||||
this.lookup("route:application").send("closeModal");
|
||||
const hideDropDownFunction = $("html").data("hide-dropdown");
|
||||
if (hideDropDownFunction) {
|
||||
hideDropDownFunction();
|
||||
}
|
||||
|
||||
this.lookup("service:app-events").trigger("dom:clean");
|
||||
this.lookup("service:document-title").updateContextCount(0);
|
||||
}
|
||||
|
||||
export function cleanDOM(container, opts) {
|
||||
scheduleOnce("afterRender", container, _clean, opts);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { isAppWebview } from "discourse/lib/utilities";
|
||||
import { later, run, schedule, throttle } from "@ember/runloop";
|
||||
import { later, run, throttle } from "@ember/runloop";
|
||||
import {
|
||||
nextTopicUrl,
|
||||
previousTopicUrl,
|
||||
@ -413,16 +413,11 @@ export default {
|
||||
|
||||
focusComposer(event) {
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
if (composer.get("model.viewOpen")) {
|
||||
preventKeyboardEvent(event);
|
||||
|
||||
schedule("afterRender", () => {
|
||||
const input = document.querySelector("textarea.d-editor-input");
|
||||
input && input.focus();
|
||||
});
|
||||
} else {
|
||||
composer.openIfDraft(event);
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
composer.focusComposer(event);
|
||||
},
|
||||
|
||||
fullscreenComposer() {
|
||||
|
||||
@ -11,6 +11,4 @@ export const PUBLIC_JS_VERSIONS = {
|
||||
"jquery.magnific-popup.min.js":
|
||||
"magnific-popup/1.1.0/jquery.magnific-popup.min.js",
|
||||
"pikaday.js": "pikaday/1.8.0/pikaday.js",
|
||||
"spectrum.js": "spectrum-colorpicker/1.8.0/spectrum.js",
|
||||
"spectrum.css": "spectrum-colorpicker/1.8.0/spectrum.css",
|
||||
};
|
||||
|
||||
@ -23,6 +23,7 @@ function getOpts(opts) {
|
||||
formatUsername,
|
||||
watchedWordsReplace: context.site.watched_words_replace,
|
||||
watchedWordsLink: context.site.watched_words_link,
|
||||
additionalOptions: context.site.markdown_additional_options,
|
||||
},
|
||||
opts
|
||||
);
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import {
|
||||
MOMENT_MONDAY,
|
||||
MOMENT_SUNDAY,
|
||||
laterThisWeek,
|
||||
laterToday,
|
||||
nextBusinessWeekStart,
|
||||
nextMonth,
|
||||
now,
|
||||
thisWeekend,
|
||||
tomorrow,
|
||||
} from "discourse/lib/time-utils";
|
||||
import I18n from "I18n";
|
||||
|
||||
export const TIME_SHORTCUT_TYPES = {
|
||||
LATER_TODAY: "later_today",
|
||||
TOMORROW: "tomorrow",
|
||||
THIS_WEEKEND: "this_weekend",
|
||||
NEXT_MONTH: "next_month",
|
||||
CUSTOM: "custom",
|
||||
RELATIVE: "relative",
|
||||
@ -29,44 +31,46 @@ export function defaultShortcutOptions(timezone) {
|
||||
id: TIME_SHORTCUT_TYPES.LATER_TODAY,
|
||||
label: "time_shortcut.later_today",
|
||||
time: laterToday(timezone),
|
||||
timeFormatted: laterToday(timezone).format(I18n.t("dates.time")),
|
||||
hidden: true,
|
||||
timeFormatKey: "dates.time",
|
||||
},
|
||||
{
|
||||
icon: "far-sun",
|
||||
id: TIME_SHORTCUT_TYPES.TOMORROW,
|
||||
label: "time_shortcut.tomorrow",
|
||||
time: tomorrow(timezone),
|
||||
timeFormatted: tomorrow(timezone).format(I18n.t("dates.time_short_day")),
|
||||
timeFormatKey: "dates.time_short_day",
|
||||
},
|
||||
{
|
||||
icon: "angle-double-right",
|
||||
id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,
|
||||
label: "time_shortcut.later_this_week",
|
||||
time: laterThisWeek(timezone),
|
||||
timeFormatted: laterThisWeek(timezone).format(
|
||||
I18n.t("dates.time_short_day")
|
||||
),
|
||||
hidden: true,
|
||||
timeFormatKey: "dates.time_short_day",
|
||||
},
|
||||
{
|
||||
icon: "bed",
|
||||
id: TIME_SHORTCUT_TYPES.THIS_WEEKEND,
|
||||
label: "time_shortcut.this_weekend",
|
||||
time: thisWeekend(timezone),
|
||||
timeFormatKey: "dates.time_short_day",
|
||||
},
|
||||
{
|
||||
icon: "briefcase",
|
||||
id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK,
|
||||
label:
|
||||
now(timezone).day() === MOMENT_MONDAY
|
||||
now(timezone).day() === MOMENT_MONDAY ||
|
||||
now(timezone).day() === MOMENT_SUNDAY
|
||||
? "time_shortcut.start_of_next_business_week_alt"
|
||||
: "time_shortcut.start_of_next_business_week",
|
||||
time: nextBusinessWeekStart(timezone),
|
||||
timeFormatted: nextBusinessWeekStart(timezone).format(
|
||||
I18n.t("dates.long_no_year")
|
||||
),
|
||||
timeFormatKey: "dates.long_no_year",
|
||||
},
|
||||
{
|
||||
icon: "far-calendar-plus",
|
||||
id: TIME_SHORTCUT_TYPES.NEXT_MONTH,
|
||||
label: "time_shortcut.next_month",
|
||||
time: nextMonth(timezone),
|
||||
timeFormatted: nextMonth(timezone).format(I18n.t("dates.long_no_year")),
|
||||
timeFormatKey: "dates.long_no_year",
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -78,7 +82,6 @@ export function specialShortcutOptions() {
|
||||
id: TIME_SHORTCUT_TYPES.LAST_CUSTOM,
|
||||
label: "time_shortcut.last_custom",
|
||||
time: null,
|
||||
timeFormatted: null,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
@ -86,7 +89,6 @@ export function specialShortcutOptions() {
|
||||
id: TIME_SHORTCUT_TYPES.CUSTOM,
|
||||
label: "time_shortcut.custom",
|
||||
time: null,
|
||||
timeFormatted: null,
|
||||
isCustomTimeShortcut: true,
|
||||
},
|
||||
{
|
||||
@ -94,7 +96,6 @@ export function specialShortcutOptions() {
|
||||
id: TIME_SHORTCUT_TYPES.NONE,
|
||||
label: "time_shortcut.none",
|
||||
time: null,
|
||||
timeFormatted: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -3,8 +3,10 @@ import { isPresent } from "@ember/utils";
|
||||
export const START_OF_DAY_HOUR = 8;
|
||||
export const LATER_TODAY_CUTOFF_HOUR = 17;
|
||||
export const LATER_TODAY_MAX_HOUR = 18;
|
||||
export const MOMENT_SUNDAY = 0;
|
||||
export const MOMENT_MONDAY = 1;
|
||||
export const MOMENT_THURSDAY = 4;
|
||||
export const MOMENT_FRIDAY = 5;
|
||||
export const MOMENT_SATURDAY = 6;
|
||||
|
||||
export function now(timezone) {
|
||||
|
||||
@ -68,7 +68,7 @@ const TIMEFRAMES = [
|
||||
buildTimeframe({
|
||||
id: "two_months",
|
||||
format: "MMM D",
|
||||
enabled: (opts) => opts.includeMidFuture,
|
||||
enabled: () => true,
|
||||
when: (time, timeOfDay) =>
|
||||
time.add(2, "month").startOf("month").hour(timeOfDay).minute(0),
|
||||
icon: "briefcase",
|
||||
@ -76,7 +76,7 @@ const TIMEFRAMES = [
|
||||
buildTimeframe({
|
||||
id: "three_months",
|
||||
format: "MMM D",
|
||||
enabled: (opts) => opts.includeMidFuture,
|
||||
enabled: () => true,
|
||||
when: (time, timeOfDay) =>
|
||||
time.add(3, "month").startOf("month").hour(timeOfDay).minute(0),
|
||||
icon: "briefcase",
|
||||
@ -84,7 +84,7 @@ const TIMEFRAMES = [
|
||||
buildTimeframe({
|
||||
id: "four_months",
|
||||
format: "MMM D",
|
||||
enabled: (opts) => opts.includeMidFuture,
|
||||
enabled: () => true,
|
||||
when: (time, timeOfDay) =>
|
||||
time.add(4, "month").startOf("month").hour(timeOfDay).minute(0),
|
||||
icon: "briefcase",
|
||||
@ -92,7 +92,7 @@ const TIMEFRAMES = [
|
||||
buildTimeframe({
|
||||
id: "six_months",
|
||||
format: "MMM D",
|
||||
enabled: (opts) => opts.includeMidFuture,
|
||||
enabled: () => true,
|
||||
when: (time, timeOfDay) =>
|
||||
time.add(6, "month").startOf("month").hour(timeOfDay).minute(0),
|
||||
icon: "briefcase",
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
import { Promise } from "rsvp";
|
||||
let model, currentTopicId;
|
||||
|
||||
let lastTopicId, lastHighestRead;
|
||||
|
||||
export function setTopicList(incomingModel) {
|
||||
model = incomingModel;
|
||||
|
||||
model?.topics?.forEach((topic) => {
|
||||
let highestRead = getHighestReadCache(topic.id);
|
||||
if (highestRead && highestRead >= topic.last_read_post_number) {
|
||||
let count = Math.max(topic.highest_post_number - highestRead, 0);
|
||||
topic.setProperties({
|
||||
unread_posts: count,
|
||||
new_posts: count,
|
||||
});
|
||||
resetHighestReadCache();
|
||||
}
|
||||
});
|
||||
currentTopicId = null;
|
||||
}
|
||||
|
||||
@ -14,6 +28,22 @@ export function previousTopicUrl() {
|
||||
return urlAt(-1);
|
||||
}
|
||||
|
||||
export function setHighestReadCache(topicId, postNumber) {
|
||||
lastTopicId = topicId;
|
||||
lastHighestRead = postNumber;
|
||||
}
|
||||
|
||||
export function getHighestReadCache(topicId) {
|
||||
if (topicId === lastTopicId) {
|
||||
return lastHighestRead;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetHighestReadCache() {
|
||||
lastTopicId = undefined;
|
||||
lastHighestRead = undefined;
|
||||
}
|
||||
|
||||
function urlAt(delta) {
|
||||
if (!model || !model.topics) {
|
||||
return Promise.resolve(null);
|
||||
|
||||
@ -207,9 +207,13 @@ export function selectedText() {
|
||||
}
|
||||
|
||||
export function selectedElement() {
|
||||
return selectedRange()?.commonAncestorContainer;
|
||||
}
|
||||
|
||||
export function selectedRange() {
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
return selection.getRangeAt(0).commonAncestorContainer;
|
||||
return selection.getRangeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,5 +503,52 @@ export function translateModKey(string) {
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
// http://github.com/feross/clipboard-copy
|
||||
export function clipboardCopy(text) {
|
||||
// Use the Async Clipboard API when available.
|
||||
// Requires a secure browsing context (i.e. HTTPS)
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text).catch(function (err) {
|
||||
throw err !== undefined
|
||||
? err
|
||||
: new DOMException("The request is not allowed", "NotAllowedError");
|
||||
});
|
||||
}
|
||||
|
||||
// ...Otherwise, use document.execCommand() fallback
|
||||
|
||||
// Put the text to copy into a <span>
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
|
||||
// Preserve consecutive spaces and newlines
|
||||
span.style.whiteSpace = "pre";
|
||||
|
||||
// Add the <span> to the page
|
||||
document.body.appendChild(span);
|
||||
|
||||
// Make a selection object representing the range of text selected by the user
|
||||
const selection = window.getSelection();
|
||||
const range = window.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
// Copy text to the clipboard
|
||||
let success = false;
|
||||
try {
|
||||
success = window.document.execCommand("copy");
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("error", err);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
selection.removeAllRanges();
|
||||
window.document.body.removeChild(span);
|
||||
return success;
|
||||
}
|
||||
|
||||
// This prevents a mini racer crash
|
||||
export default {};
|
||||
|
||||
@ -11,6 +11,7 @@ export default Mixin.create({
|
||||
bulkSelectEnabled: false,
|
||||
autoAddTopicsToBulkSelect: false,
|
||||
selected: null,
|
||||
lastChecked: null,
|
||||
|
||||
canBulkSelect: or("currentUser.staff", "showDismissRead", "showResetNew"),
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
} from "discourse/lib/uploads";
|
||||
import { cacheShortUploadUrl } from "pretty-text/upload-short-url";
|
||||
import bootbox from "bootbox";
|
||||
import { run } from "@ember/runloop";
|
||||
|
||||
// Note: This mixin is used _in addition_ to the ComposerUpload mixin
|
||||
// on the composer-editor component. It overrides some, but not all,
|
||||
@ -64,7 +65,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
this.fileInputEventListener
|
||||
);
|
||||
|
||||
this.element.removeEventListener("paste", this.pasteEventListener);
|
||||
this.editorEl?.removeEventListener("paste", this.pasteEventListener);
|
||||
|
||||
this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles);
|
||||
this.appEvents.off(
|
||||
@ -92,6 +93,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
this.set("inProgressUploads", []);
|
||||
this.placeholders = {};
|
||||
this._preProcessorStatus = {};
|
||||
this.editorEl = this.element.querySelector(this.editorClass);
|
||||
this.fileInputEl = document.getElementById(this.fileUploadElementId);
|
||||
const isPrivateMessage = this.get("composerModel.privateMessage");
|
||||
|
||||
@ -106,7 +108,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
this.fileInputEl,
|
||||
this._addFiles
|
||||
);
|
||||
this.element.addEventListener("paste", this.pasteEventListener);
|
||||
this.editorEl.addEventListener("paste", this.pasteEventListener);
|
||||
|
||||
this._uppyInstance = new Uppy({
|
||||
id: this.uppyId,
|
||||
@ -206,112 +208,135 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
}
|
||||
|
||||
this._uppyInstance.on("file-added", (file) => {
|
||||
if (isPrivateMessage) {
|
||||
file.meta.for_private_message = true;
|
||||
}
|
||||
run(() => {
|
||||
if (isPrivateMessage) {
|
||||
file.meta.for_private_message = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("progress", (progress) => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
run(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("uploadProgress", progress);
|
||||
this.set("uploadProgress", progress);
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("file-removed", (file, reason) => {
|
||||
// we handle the cancel-all event specifically, so no need
|
||||
// to do anything here. this event is also fired when some files
|
||||
// are handled by an upload handler
|
||||
if (reason === "cancel-all") {
|
||||
return;
|
||||
}
|
||||
run(() => {
|
||||
// we handle the cancel-all event specifically, so no need
|
||||
// to do anything here. this event is also fired when some files
|
||||
// are handled by an upload handler
|
||||
if (reason === "cancel-all") {
|
||||
return;
|
||||
}
|
||||
|
||||
file.meta.cancelled = true;
|
||||
this._removeInProgressUpload(file.id);
|
||||
this._resetUpload(file, { removePlaceholder: true });
|
||||
if (this.inProgressUploads.length === 0) {
|
||||
this.set("userCancelled", true);
|
||||
this._uppyInstance.cancelAll();
|
||||
}
|
||||
file.meta.cancelled = true;
|
||||
this._removeInProgressUpload(file.id);
|
||||
this._resetUpload(file, { removePlaceholder: true });
|
||||
if (this.inProgressUploads.length === 0) {
|
||||
this.set("userCancelled", true);
|
||||
this._uppyInstance.cancelAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("upload-progress", (file, progress) => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
run(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
|
||||
if (upload) {
|
||||
const percentage = Math.round(
|
||||
(progress.bytesUploaded / progress.bytesTotal) * 100
|
||||
);
|
||||
upload.set("progress", percentage);
|
||||
}
|
||||
const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
|
||||
if (upload) {
|
||||
const percentage = Math.round(
|
||||
(progress.bytesUploaded / progress.bytesTotal) * 100
|
||||
);
|
||||
upload.set("progress", percentage);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("upload", (data) => {
|
||||
this._addNeedProcessing(data.fileIDs.length);
|
||||
run(() => {
|
||||
this._addNeedProcessing(data.fileIDs.length);
|
||||
|
||||
const files = data.fileIDs.map((fileId) =>
|
||||
this._uppyInstance.getFile(fileId)
|
||||
);
|
||||
|
||||
this.setProperties({
|
||||
isProcessingUpload: true,
|
||||
isCancellable: false,
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
// The inProgressUploads is meant to be used to display these uploads
|
||||
// in a UI, and Ember will only update the array in the UI if pushObject
|
||||
// is used to notify it.
|
||||
this.inProgressUploads.pushObject(
|
||||
EmberObject.create({
|
||||
fileName: file.name,
|
||||
id: file.id,
|
||||
progress: 0,
|
||||
extension: file.extension,
|
||||
})
|
||||
const files = data.fileIDs.map((fileId) =>
|
||||
this._uppyInstance.getFile(fileId)
|
||||
);
|
||||
const placeholder = this._uploadPlaceholder(file);
|
||||
this.placeholders[file.id] = {
|
||||
uploadPlaceholder: placeholder,
|
||||
};
|
||||
this.appEvents.trigger(`${this.eventPrefix}:insert-text`, placeholder);
|
||||
this.appEvents.trigger(`${this.eventPrefix}:upload-started`, file.name);
|
||||
|
||||
this.setProperties({
|
||||
isProcessingUpload: true,
|
||||
isCancellable: false,
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
// The inProgressUploads is meant to be used to display these uploads
|
||||
// in a UI, and Ember will only update the array in the UI if pushObject
|
||||
// is used to notify it.
|
||||
this.inProgressUploads.pushObject(
|
||||
EmberObject.create({
|
||||
fileName: file.name,
|
||||
id: file.id,
|
||||
progress: 0,
|
||||
extension: file.extension,
|
||||
})
|
||||
);
|
||||
const placeholder = this._uploadPlaceholder(file);
|
||||
this.placeholders[file.id] = {
|
||||
uploadPlaceholder: placeholder,
|
||||
};
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:insert-text`,
|
||||
placeholder
|
||||
);
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:upload-started`,
|
||||
file.name
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("upload-success", (file, response) => {
|
||||
this._removeInProgressUpload(file.id);
|
||||
let upload = response.body;
|
||||
const markdown = this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
run(() => {
|
||||
if (!this._uppyInstance) {
|
||||
return;
|
||||
}
|
||||
this._removeInProgressUpload(file.id);
|
||||
let upload = response.body;
|
||||
const markdown = this.uploadMarkdownResolvers.reduce(
|
||||
(md, resolver) => resolver(upload) || md,
|
||||
getUploadMarkdown(upload)
|
||||
);
|
||||
|
||||
cacheShortUploadUrl(upload.short_url, upload);
|
||||
cacheShortUploadUrl(upload.short_url, upload);
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
this.placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
this.placeholders[file.id].uploadPlaceholder.trim(),
|
||||
markdown
|
||||
);
|
||||
|
||||
this._resetUpload(file, { removePlaceholder: false });
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:upload-success`,
|
||||
file.name,
|
||||
upload
|
||||
);
|
||||
this._resetUpload(file, { removePlaceholder: false });
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:upload-success`,
|
||||
file.name,
|
||||
upload
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("upload-error", this._handleUploadError);
|
||||
|
||||
this._uppyInstance.on("complete", () => {
|
||||
this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`);
|
||||
this._reset();
|
||||
run(() => {
|
||||
this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`);
|
||||
this._reset();
|
||||
});
|
||||
});
|
||||
|
||||
this._uppyInstance.on("cancel-all", () => {
|
||||
@ -319,11 +344,13 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
// only do the manual cancelling work if the user clicked cancel
|
||||
if (this.userCancelled) {
|
||||
Object.values(this.placeholders).forEach((data) => {
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
data.uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
run(() => {
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
data.uploadPlaceholder,
|
||||
""
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.set("userCancelled", false);
|
||||
@ -415,21 +442,25 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
|
||||
this._onPreProcessComplete(
|
||||
(file) => {
|
||||
let placeholderData = this.placeholders[file.id];
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
run(() => {
|
||||
let placeholderData = this.placeholders[file.id];
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:replace-text`,
|
||||
placeholderData.processingPlaceholder,
|
||||
placeholderData.uploadPlaceholder
|
||||
);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setProperties({
|
||||
isProcessingUpload: false,
|
||||
isCancellable: true,
|
||||
run(() => {
|
||||
this.setProperties({
|
||||
isProcessingUpload: false,
|
||||
isCancellable: true,
|
||||
});
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:uploads-preprocessing-complete`
|
||||
);
|
||||
});
|
||||
this.appEvents.trigger(
|
||||
`${this.eventPrefix}:uploads-preprocessing-complete`
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
@ -520,12 +551,12 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
return;
|
||||
}
|
||||
|
||||
const { canUpload } = clipboardHelpers(event, {
|
||||
const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
|
||||
siteSettings: this.siteSettings,
|
||||
canUpload: true,
|
||||
});
|
||||
|
||||
if (!canUpload) {
|
||||
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -25,18 +25,16 @@ export default Mixin.create({
|
||||
|
||||
// ensures textarea scroll position is correct
|
||||
_focusTextArea() {
|
||||
schedule("afterRender", () => {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._textarea) {
|
||||
return;
|
||||
}
|
||||
if (!this._textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._textarea.blur();
|
||||
this._textarea.focus();
|
||||
});
|
||||
this._textarea.blur();
|
||||
this._textarea.focus();
|
||||
},
|
||||
|
||||
_insertBlock(text) {
|
||||
@ -171,7 +169,7 @@ export default Mixin.create({
|
||||
this._$textarea.prop("selectionStart", (pre + text).length + 2);
|
||||
this._$textarea.prop("selectionEnd", (pre + text).length + 2);
|
||||
|
||||
this._focusTextArea();
|
||||
schedule("afterRender", this, this._focusTextArea);
|
||||
},
|
||||
|
||||
_addText(sel, text, options) {
|
||||
|
||||
@ -542,6 +542,7 @@ Category.reopenClass({
|
||||
|
||||
search(term, opts) {
|
||||
let limit = 5;
|
||||
let parentCategoryId;
|
||||
|
||||
if (opts) {
|
||||
if (opts.limit === 0) {
|
||||
@ -549,6 +550,9 @@ Category.reopenClass({
|
||||
} else if (opts.limit) {
|
||||
limit = opts.limit;
|
||||
}
|
||||
if (opts.parentCategoryId) {
|
||||
parentCategoryId = opts.parentCategoryId;
|
||||
}
|
||||
}
|
||||
|
||||
const emptyTerm = term === "";
|
||||
@ -569,13 +573,21 @@ Category.reopenClass({
|
||||
return data.length === limit;
|
||||
};
|
||||
|
||||
const validCategoryParent = (category) => {
|
||||
return (
|
||||
!parentCategoryId ||
|
||||
category.get("parent_category_id") === parentCategoryId
|
||||
);
|
||||
};
|
||||
|
||||
for (i = 0; i < length && !done(); i++) {
|
||||
const category = categories[i];
|
||||
if (
|
||||
(emptyTerm && !category.get("parent_category_id")) ||
|
||||
(!emptyTerm &&
|
||||
(category.get("name").toLowerCase().indexOf(term) === 0 ||
|
||||
category.get("slug").toLowerCase().indexOf(slugTerm) === 0))
|
||||
((emptyTerm && !category.get("parent_category_id")) ||
|
||||
(!emptyTerm &&
|
||||
(category.get("name").toLowerCase().indexOf(term) === 0 ||
|
||||
category.get("slug").toLowerCase().indexOf(slugTerm) === 0))) &&
|
||||
validCategoryParent(category)
|
||||
) {
|
||||
data.push(category);
|
||||
}
|
||||
@ -586,9 +598,10 @@ Category.reopenClass({
|
||||
const category = categories[i];
|
||||
|
||||
if (
|
||||
!emptyTerm &&
|
||||
(category.get("name").toLowerCase().indexOf(term) > 0 ||
|
||||
category.get("slug").toLowerCase().indexOf(slugTerm) > 0)
|
||||
((!emptyTerm &&
|
||||
category.get("name").toLowerCase().indexOf(term) > 0) ||
|
||||
category.get("slug").toLowerCase().indexOf(slugTerm) > 0) &&
|
||||
validCategoryParent(category)
|
||||
) {
|
||||
if (data.indexOf(category) === -1) {
|
||||
data.push(category);
|
||||
|
||||
@ -660,6 +660,14 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (opts && opts.new_line) {
|
||||
if (before.length > 0) {
|
||||
text = "\n\n" + text.trim();
|
||||
} else {
|
||||
text = text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
this.set("reply", before + text + after);
|
||||
|
||||
return before.length + text.length;
|
||||
|
||||
@ -243,6 +243,7 @@ const Group = RestModel.extend({
|
||||
imap_mailbox_name: this.imap_mailbox_name,
|
||||
imap_enabled: this.imap_enabled,
|
||||
email_username: this.email_username,
|
||||
email_from_alias: this.email_from_alias,
|
||||
email_password: this.email_password,
|
||||
flair_icon: null,
|
||||
flair_upload_id: null,
|
||||
|
||||
@ -194,6 +194,7 @@ const TopicTrackingState = EmberObject.extend({
|
||||
|
||||
const filter = this.filter;
|
||||
const filterCategory = this.filterCategory;
|
||||
const filterTag = this.filterTag;
|
||||
const categoryId = data.payload && data.payload.category_id;
|
||||
|
||||
// if we have a filter category currently and it is not the
|
||||
@ -209,6 +210,10 @@ const TopicTrackingState = EmberObject.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (filterTag && !data.payload.tags.includes(filterTag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// always count a new_topic as incoming
|
||||
if (
|
||||
["all", "latest", "new", "unseen"].includes(filter) &&
|
||||
@ -275,25 +280,34 @@ const TopicTrackingState = EmberObject.extend({
|
||||
* @method trackIncoming
|
||||
* @param {String} filter - Valid values are all, categories, and any topic list
|
||||
* filters e.g. latest, unread, new. As well as this
|
||||
* specific category and tag URLs like /tag/test/l/latest
|
||||
* or c/cat/subcat/6/l/latest.
|
||||
* specific category and tag URLs like tag/test/l/latest,
|
||||
* c/cat/subcat/6/l/latest or tags/c/cat/subcat/6/test/l/latest.
|
||||
*/
|
||||
trackIncoming(filter) {
|
||||
this.newIncoming = [];
|
||||
|
||||
if (filter.startsWith("c/")) {
|
||||
const categoryId = filter.match(/\/(\d*)\//);
|
||||
const category = Category.findById(parseInt(categoryId[1], 10));
|
||||
this.set("filterCategory", category);
|
||||
let category, tag;
|
||||
|
||||
if (filter.startsWith("c/") || filter.startsWith("tags/c/")) {
|
||||
const categoryId = filter.match(/\/(\d*)\//);
|
||||
category = Category.findById(parseInt(categoryId[1], 10));
|
||||
const split = filter.split("/");
|
||||
|
||||
if (filter.startsWith("tags/c/")) {
|
||||
tag = split[split.indexOf(categoryId[1]) + 1];
|
||||
}
|
||||
|
||||
if (split.length >= 4) {
|
||||
filter = split[split.length - 1];
|
||||
}
|
||||
} else {
|
||||
this.set("filterCategory", null);
|
||||
} else if (filter.startsWith("tag/")) {
|
||||
const split = filter.split("/");
|
||||
filter = split[split.length - 1];
|
||||
tag = split[1];
|
||||
}
|
||||
|
||||
this.set("filterCategory", category);
|
||||
this.set("filterTag", tag);
|
||||
this.set("filter", filter);
|
||||
this.set("incomingCount", 0);
|
||||
},
|
||||
|
||||
@ -334,13 +334,16 @@ const User = RestModel.extend({
|
||||
userFields.filter((uf) => !fields || fields.indexOf(uf) !== -1)
|
||||
);
|
||||
|
||||
let filteredUserOptionFields = [];
|
||||
if (fields) {
|
||||
userOptionFields = userOptionFields.filter(
|
||||
filteredUserOptionFields = userOptionFields.filter(
|
||||
(uo) => fields.indexOf(uo) !== -1
|
||||
);
|
||||
} else {
|
||||
filteredUserOptionFields = userOptionFields;
|
||||
}
|
||||
|
||||
userOptionFields.forEach((s) => {
|
||||
filteredUserOptionFields.forEach((s) => {
|
||||
data[s] = this.get(`user_option.${s}`);
|
||||
});
|
||||
|
||||
@ -379,6 +382,10 @@ const User = RestModel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
return this._saveUserData(data, updatedState);
|
||||
},
|
||||
|
||||
_saveUserData(data, updatedState) {
|
||||
// TODO: We can remove this when migrated fully to rest model.
|
||||
this.set("isSaving", true);
|
||||
return ajax(userPath(`${this.username_lower}.json`), {
|
||||
|
||||
@ -2,6 +2,11 @@ import Service, { inject as service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import {
|
||||
getHighestReadCache,
|
||||
resetHighestReadCache,
|
||||
setHighestReadCache,
|
||||
} from "discourse/lib/topic-list-tracker";
|
||||
|
||||
// We use this class to track how long posts in a topic are on the screen.
|
||||
const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3;
|
||||
@ -128,9 +133,19 @@ export default class ScreenTrack extends Service {
|
||||
this._consolidatedTimings.push({ timings, topicTime, topicId });
|
||||
}
|
||||
|
||||
const highestRead = parseInt(Object.keys(timings).lastObject, 10);
|
||||
const cachedHighestRead = this.highestReadFromCache(topicId);
|
||||
if (!cachedHighestRead || cachedHighestRead < highestRead) {
|
||||
setHighestReadCache(topicId, highestRead);
|
||||
}
|
||||
|
||||
return this._consolidatedTimings;
|
||||
}
|
||||
|
||||
highestReadFromCache(topicId) {
|
||||
return getHighestReadCache(topicId);
|
||||
}
|
||||
|
||||
sendNextConsolidatedTiming() {
|
||||
if (this._consolidatedTimings.length === 0) {
|
||||
return;
|
||||
@ -172,11 +187,19 @@ export default class ScreenTrack extends Service {
|
||||
if (topicController) {
|
||||
const postNumbers = Object.keys(timings).map((v) => parseInt(v, 10));
|
||||
topicController.readPosts(topicId, postNumbers);
|
||||
|
||||
const cachedHighestRead = this.highestReadFromCache(topicId);
|
||||
if (
|
||||
cachedHighestRead &&
|
||||
cachedHighestRead <= postNumbers.lastObject
|
||||
) {
|
||||
resetHighestReadCache(topicId);
|
||||
}
|
||||
}
|
||||
this.appEvents.trigger("topic:timings-sent", data);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (ALLOWED_AJAX_FAILURES.indexOf(e.jqXHR.status) > -1) {
|
||||
if (e.jqXHR && ALLOWED_AJAX_FAILURES.indexOf(e.jqXHR.status) > -1) {
|
||||
const delay = AJAX_FAILURE_DELAYS[this._ajaxFailures];
|
||||
this._ajaxFailures += 1;
|
||||
|
||||
@ -187,7 +210,7 @@ export default class ScreenTrack extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
if (window.console && window.console.warn) {
|
||||
if (window.console && window.console.warn && e.jqXHR) {
|
||||
window.console.warn(
|
||||
`Failed to update topic times for topic ${topicId} due to ${e.jqXHR.status} error`
|
||||
);
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
customOptions=customTimeShortcutOptions
|
||||
hiddenOptions=hiddenTimeShortcutOptions
|
||||
customLabels=customTimeShortcutLabels
|
||||
additionalOptionsToShow=additionalTimeShortcutOptions
|
||||
_itsatrap=_itsatrap
|
||||
}}
|
||||
{{else}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#each categories as |c|}}
|
||||
<div data-notification-level={{c.notificationLevelString}} style={{unless noCategoryStyle (border-color c.color)}} class="category category-box category-box-{{c.slug}} {{if c.isMuted "muted"}} {{if noCategoryStyle "no-category-boxes-style"}}">
|
||||
<div data-notification-level={{c.notificationLevelString}} style={{unless noCategoryStyle (concat (border-color c.color) (category-color-variable c.color))}} class="category category-box category-box-{{c.slug}} {{if c.isMuted "muted"}} {{if noCategoryStyle "no-category-boxes-style"}}">
|
||||
<div class="category-box-inner">
|
||||
<div class="category-box-heading">
|
||||
<a href={{c.url}}>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{{#each categories as |c|}}
|
||||
<div style={{unless noCategoryStyle (border-color c.color)}} data-category-id={{c.id}} data-notification-level={{c.notificationLevelString}} data-url={{c.url}} class="category category-box category-box-{{c.slug}} {{if c.isMuted "muted"}} {{if noCategoryStyle "no-category-boxes-style"}}">
|
||||
{{plugin-outlet name="category-box-before-each-box" args=(hash category=c)}}
|
||||
<div style={{unless noCategoryStyle (concat (border-color c.color) (category-color-variable c.color))}} data-category-id={{c.id}} data-notification-level={{c.notificationLevelString}} data-url={{c.url}} class="category category-box category-box-{{c.slug}} {{if c.isMuted "muted"}} {{if noCategoryStyle "no-category-boxes-style"}}">
|
||||
<div class="category-box-inner">
|
||||
{{#unless c.isMuted}}
|
||||
<div class="category-logo">
|
||||
@ -73,4 +74,5 @@
|
||||
{{plugin-outlet name="category-box-below-each-category" args=(hash category=c)}}
|
||||
</div>
|
||||
</div>
|
||||
{{plugin-outlet name="category-box-after-each-box" args=(hash category=c)}}
|
||||
{{/each}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{{text-field
|
||||
{{#if onlyHex}}<span class="add-on">#</span>{{/if}}{{text-field
|
||||
class="hex-input"
|
||||
value=hexValue
|
||||
maxlength=maxlength
|
||||
input=(action "onHexInput" value="target.value")
|
||||
}}
|
||||
<input class="picker" type="input">
|
||||
<input class="picker" type="color" value={{normalizedHexValue}} {{on "input" this.onPickerInput}}>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
placeholderKey=composer.titlePlaceholder
|
||||
aria-label=(I18n composer.titlePlaceholder)
|
||||
disabled=disabled
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
}}
|
||||
|
||||
{{popup-input-tip validation=validation}}
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{d-textarea
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
tabindex=tabindex
|
||||
value=value
|
||||
class="d-editor-input"
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<section class="field">
|
||||
<span class="color-title">{{i18n "category.background_color"}}:</span>
|
||||
<div class="colorpicker-wrapper">
|
||||
<span class="add-on">#</span>{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-input hexValue=category.color valid=category.colorValid}}
|
||||
{{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}}
|
||||
</div>
|
||||
</section>
|
||||
@ -72,7 +72,7 @@
|
||||
<section class="field">
|
||||
<span class="color-title">{{i18n "category.foreground_color"}}:</span>
|
||||
<div class="colorpicker-wrapper edit-text-color">
|
||||
<span class="add-on">#</span>{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
|
||||
{{color-input hexValue=category.text_color}}
|
||||
{{color-picker colors=foregroundColors value=category.text_color id="edit-text-color"}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
class="filter"
|
||||
name="filter"
|
||||
placeholder=(i18n "emoji_picker.filter_placeholder")
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
type="search"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
input=(action "onFilter")
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
includeDateTime=includeDateTime
|
||||
includeWeekend=includeWeekend
|
||||
includeFarFuture=includeFarFuture
|
||||
includeMidFuture=includeMidFuture
|
||||
includeNow=includeNow
|
||||
clearable=clearable
|
||||
onChangeInput=onChangeInput
|
||||
|
||||
@ -45,5 +45,5 @@
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{{group-manage-save-button model=group disabled=(not emailSettingsValid) beforeSave=beforeSave afterSave=afterSave tabindex="14"}}
|
||||
{{group-manage-save-button model=group disabled=(not emailSettingsValid) beforeSave=beforeSave afterSave=afterSave tabindex="15"}}
|
||||
</div>
|
||||
|
||||
@ -8,11 +8,11 @@
|
||||
|
||||
<div class="control-group">
|
||||
<label for="smtp_server">{{i18n "groups.manage.email.credentials.smtp_server"}}</label>
|
||||
{{input type="text" name="smtp_server" value=form.smtp_server tabindex="3" onChange=(action "resetSettingsValid")}}
|
||||
{{input type="text" name="smtp_server" value=form.smtp_server tabindex="4" onChange=(action "resetSettingsValid")}}
|
||||
</div>
|
||||
|
||||
<label for="enable_ssl">
|
||||
{{input type="checkbox" checked=form.smtp_ssl id="enable_ssl" tabindex="5" onChange=(action "resetSettingsValid")}}
|
||||
{{input type="checkbox" checked=form.smtp_ssl id="enable_ssl" tabindex="6" onChange=(action "resetSettingsValid")}}
|
||||
{{i18n "groups.manage.email.credentials.smtp_ssl"}}
|
||||
</label>
|
||||
</div>
|
||||
@ -25,7 +25,15 @@
|
||||
|
||||
<div class="control-group">
|
||||
<label for="smtp_port">{{i18n "groups.manage.email.credentials.smtp_port"}}</label>
|
||||
{{input type="text" name="smtp_port" value=form.smtp_port tabindex="4" onChange=(action "resetSettingsValid" form.smtp_port)}}
|
||||
{{input type="text" name="smtp_port" value=form.smtp_port tabindex="5" onChange=(action "resetSettingsValid" form.smtp_port)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="control-group">
|
||||
<label for="from_alias">{{i18n "groups.manage.email.settings.from_alias"}}</label>
|
||||
{{input type="text" name="from_alias" id="from_alias" value=form.email_from_alias onChange=(action "resetSettingsValid") tabindex="3"}}
|
||||
<p>{{i18n "groups.manage.email.settings.from_alias_hint"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -43,7 +51,7 @@
|
||||
action=(action "testSmtpSettings")
|
||||
icon="cog"
|
||||
label="groups.manage.email.test_settings"
|
||||
tabindex="6"
|
||||
tabindex="7"
|
||||
title="groups.manage.email.settings_required"
|
||||
}}
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@
|
||||
</label>
|
||||
{{future-date-input
|
||||
includeDateTime=true
|
||||
includeMidFuture=true
|
||||
clearable=true
|
||||
onChangeInput=(action (mut inviteExpiresAt))
|
||||
}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{{#unless isHidden}}
|
||||
{{plugin-outlet name="category-list-above-each-category" args=(hash category=category)}}
|
||||
<tr data-category-id={{category.id}} data-notification-level={{category.notificationLevelString}} class="{{if category.description_excerpt "has-description" "no-description"}} {{if category.uploaded_logo.url "has-logo" "no-logo"}}">
|
||||
<td class="category {{if isMuted "muted"}} {{if noCategoryStyle "no-category-style"}}" style={{unless noCategoryStyle (border-color category.color)}}>
|
||||
<td class="category {{if isMuted "muted"}} {{if noCategoryStyle "no-category-style"}}" style={{unless noCategoryStyle (concat (border-color category.color) (category-color-variable category.color))}}>
|
||||
{{category-title-link category=category}}
|
||||
{{plugin-outlet name="below-category-title-link" connectorTagName="div" args=(hash category=category)}}
|
||||
{{#if category.description_excerpt}}
|
||||
|
||||
@ -1 +1,6 @@
|
||||
{{topicListItemContents}}
|
||||
|
||||
{{plugin-outlet
|
||||
name="after-topic-list-item"
|
||||
args=(hash topic=topic)
|
||||
}}
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
expandAllPinned=expandAllPinned
|
||||
lastVisitedTopic=lastVisitedTopic
|
||||
selected=selected
|
||||
lastChecked=lastChecked
|
||||
tagsForUser=tagsForUser}}
|
||||
{{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}}
|
||||
{{/each}}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{{text-field
|
||||
value=filterInput
|
||||
placeholderKey=filterPlaceholder
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
class="group-username-filter no-blur"
|
||||
}}
|
||||
{{/if}}
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
{{/if}}
|
||||
|
||||
<div class="input username-input input-group">
|
||||
{{input value=accountUsername class=(value-entered accountUsername) id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="discourse"}}
|
||||
{{input value=accountUsername class=(value-entered accountUsername) id="new-account-username" name="username" maxlength=maxUsernameLength autocomplete="off"}}
|
||||
<label class="alt-placeholder" for="new-account-username">
|
||||
{{i18n "user.username.title"}}
|
||||
<span class="required">*</span>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{{text-field
|
||||
value=filterInput
|
||||
placeholderKey=filterPlaceholder
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
class="group-username-filter no-blur"
|
||||
}}
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
maxlength=maxUsernameLength
|
||||
aria-describedby="username-validation"
|
||||
aria-invalid=usernameValidation.failed
|
||||
autocomplete="discourse"
|
||||
autocomplete="off"
|
||||
}}
|
||||
<label class="alt-placeholder" for="new-account-username">
|
||||
{{i18n "user.username.title"}}
|
||||
|
||||
@ -124,7 +124,6 @@
|
||||
displayLabel=(i18n "user.invited.invite.expires_at")
|
||||
statusType="close"
|
||||
includeDateTime=true
|
||||
includeMidFuture=true
|
||||
clearable=true
|
||||
input=buffered.expires_at
|
||||
onChangeInput=(action (mut buffered.expires_at))
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
input=(readonly ignoredUntil)
|
||||
includeWeekend=true
|
||||
includeDateTime=false
|
||||
includeMidFuture=true
|
||||
includeFarFuture=true
|
||||
onChangeInput=(action (mut ignoredUntil))
|
||||
}}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
input=ignoredUntil
|
||||
includeWeekend=true
|
||||
includeDateTime=false
|
||||
includeMidFuture=true
|
||||
includeFarFuture=true
|
||||
onChangeInput=(action (mut ignoredUntil))
|
||||
}}
|
||||
|
||||
@ -62,6 +62,14 @@
|
||||
<div class="top-lists">
|
||||
{{period-chooser period=period action=(action "changePeriod") fullDay=false}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if topicTrackingState.hasIncoming}}
|
||||
<div class="show-more {{if hasTopics "has-topics"}}">
|
||||
<a tabindex="0" href {{action "showInserted"}} class="alert alert-info clickable">
|
||||
{{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}}
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if list.topics}}
|
||||
|
||||
@ -10,9 +10,11 @@
|
||||
{{#d-section class="user-invite-buttons"}}
|
||||
{{d-button class="btn-default" icon="plus" action=(action "createInvite") label="user.invited.create"}}
|
||||
{{#if canBulkInvite}}
|
||||
{{#unless site.mobileView}}
|
||||
{{d-button class="btn-default" icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}}
|
||||
{{/unless}}
|
||||
{{#if siteSettings.allow_bulk_invite}}
|
||||
{{#unless site.mobileView}}
|
||||
{{d-button class="btn-default" icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if showBulkActionButtons}}
|
||||
{{#if inviteExpired}}
|
||||
|
||||
@ -179,6 +179,7 @@
|
||||
{{plugin-outlet name="user-profile-primary" tagName="span" connectorTagName="div" args=(hash model=model)}}
|
||||
</div>
|
||||
</div>
|
||||
{{plugin-outlet name="user-profile-above-collapsed-info" args=(hash model=model collapsedInfo=collapsedInfo)}}
|
||||
{{#unless collapsedInfo}}
|
||||
<div class='secondary' id='collapsed-info-panel'>
|
||||
<dl>
|
||||
|
||||
@ -12,7 +12,9 @@
|
||||
value=searchTerm
|
||||
placeholder=(i18n "bookmarks.search_placeholder")
|
||||
enter=(action "search")
|
||||
id="bookmark-search" autocomplete="discourse"}}
|
||||
id="bookmark-search"
|
||||
autocomplete="off"
|
||||
}}
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
action=(action "search")
|
||||
|
||||
@ -4,48 +4,48 @@
|
||||
<div class="top-section stats-section">
|
||||
<h3 class="stats-title">{{i18n "user.summary.stats"}}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<li class="stats-days-visited">
|
||||
{{user-stat value=model.days_visited label="user.summary.days_visited"}}
|
||||
</li>
|
||||
<li>
|
||||
<li class="stats-time-read">
|
||||
{{user-stat value=timeRead label="user.summary.time_read" type="string"}}
|
||||
</li>
|
||||
{{#if showRecentTimeRead}}
|
||||
<li>
|
||||
<li class="stats-recent-read">
|
||||
{{user-stat value=recentTimeRead label="user.summary.recent_time_read" type="string"}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<li class="stats-topics-entered">
|
||||
{{user-stat value=model.topics_entered label="user.summary.topics_entered"}}
|
||||
</li>
|
||||
<li>
|
||||
<li class="stats-posts-read">
|
||||
{{user-stat value=model.posts_read_count label="user.summary.posts_read"}}
|
||||
</li>
|
||||
<li class="linked-stat">
|
||||
<li class="stats-likes-given linked-stat">
|
||||
{{#link-to "userActivity.likesGiven"}}
|
||||
{{user-stat value=model.likes_given icon="heart" label="user.summary.likes_given"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="stats-likes-received">
|
||||
{{user-stat value=model.likes_received icon="heart" label="user.summary.likes_received"}}
|
||||
</li>
|
||||
{{#if model.bookmark_count}}
|
||||
<li class="linked-stat">
|
||||
<li class="stats-bookmark-count linked-stat">
|
||||
{{#link-to "userActivity.bookmarks"}}
|
||||
{{user-stat value=model.bookmark_count label="user.summary.bookmark_count"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li class="linked-stat">
|
||||
<li class="stats-topic-count linked-stat">
|
||||
{{#link-to "userActivity.topics"}}
|
||||
{{user-stat value=model.topic_count label="user.summary.topic_count"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="linked-stat">
|
||||
<li class="stats-post-count linked-stat">
|
||||
{{#link-to "userActivity.replies"}}
|
||||
{{user-stat value=model.post_count label="user.summary.post_count"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{user-stat value=model.likes_received icon="heart" label="user.summary.likes_received"}}
|
||||
</li>
|
||||
{{plugin-outlet name="user-summary-stat" connectorTagName="li" args=(hash model=model)}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import { Promise } from "rsvp";
|
||||
import Session from "discourse/models/session";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
import { postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
|
||||
/**
|
||||
* This tries to enforce a consistent flow of fetching, caching, refreshing,
|
||||
@ -75,6 +76,7 @@ export default createWidget("quick-access-panel", {
|
||||
markRead() {
|
||||
return this.markReadRequest().then(() => {
|
||||
this.refreshNotifications(this.state);
|
||||
postRNWebviewMessage("markRead", "1");
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -71,13 +71,7 @@ module.exports = function (defaults) {
|
||||
});
|
||||
|
||||
let tests = concat(appTestTrees, {
|
||||
inputFiles: [
|
||||
"**/tests/acceptance/*.js",
|
||||
"**/tests/integration/*.js",
|
||||
"**/tests/integration/**/*.js",
|
||||
"**/tests/unit/*.js",
|
||||
"**/tests/unit/**/*.js",
|
||||
],
|
||||
inputFiles: ["**/tests/**/*-test.js"],
|
||||
headerFiles: ["vendor/ember-cli/tests-prefix.js"],
|
||||
footerFiles: ["vendor/ember-cli/app-config.js"],
|
||||
outputFile: "/assets/core-tests.js",
|
||||
@ -104,13 +98,17 @@ module.exports = function (defaults) {
|
||||
// For example: our very specific version of bootstrap-modal.
|
||||
app.import(vendorJs + "bootbox.js");
|
||||
app.import(vendorJs + "bootstrap-modal.js");
|
||||
app.import(vendorJs + "jquery.ui.widget.js");
|
||||
app.import(vendorJs + "caret_position.js");
|
||||
app.import("node_modules/ember-source/dist/ember-template-compiler.js", {
|
||||
type: "test",
|
||||
});
|
||||
app.import(discourseRoot + "/app/assets/javascripts/polyfills.js");
|
||||
|
||||
app.import(
|
||||
discourseRoot +
|
||||
"/app/assets/javascripts/discourse/public/assets/scripts/module-shims.js"
|
||||
);
|
||||
|
||||
const mergedTree = mergeTrees([
|
||||
discourseScss(`${discourseRoot}/app/assets/stylesheets`, "testem.scss"),
|
||||
createI18nTree(discourseRoot, vendorJs),
|
||||
|
||||
1
app/assets/javascripts/discourse/lib/rfc176-shims/.npmrc
Normal file
1
app/assets/javascripts/discourse/lib/rfc176-shims/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict = true
|
||||
57
app/assets/javascripts/discourse/lib/rfc176-shims/index.js
Normal file
57
app/assets/javascripts/discourse/lib/rfc176-shims/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
// In core, babel-plugin-ember-modules-api-polyfill takes care of re-writing the new module
|
||||
// syntax to the legacy Ember globals. For themes and plugins, we need to manually set up
|
||||
// the modules.
|
||||
//
|
||||
// Eventually, Ember RFC176 will be implemented, and we can drop these shims.
|
||||
|
||||
const RFC176Data = require("ember-rfc176-data");
|
||||
|
||||
module.exports = {
|
||||
name: require("./package").name,
|
||||
|
||||
isDevelopingAddon() {
|
||||
return true;
|
||||
},
|
||||
|
||||
contentFor: function (type) {
|
||||
if (type !== "vendor-suffix") {
|
||||
return;
|
||||
}
|
||||
|
||||
const modules = {};
|
||||
|
||||
for (const entry of RFC176Data) {
|
||||
// Entries look like:
|
||||
// {
|
||||
// global: 'Ember.expandProperties',
|
||||
// module: '@ember/object/computed',
|
||||
// export: 'expandProperties',
|
||||
// deprecated: false
|
||||
// },
|
||||
|
||||
if (entry.deprecated) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let m = modules[entry.module];
|
||||
if (!m) {
|
||||
m = modules[entry.module] = [];
|
||||
}
|
||||
|
||||
m.push(entry);
|
||||
}
|
||||
|
||||
let output = "";
|
||||
for (const moduleName of Object.keys(modules)) {
|
||||
const exports = modules[moduleName];
|
||||
const rawExports = exports
|
||||
.map((e) => `${e.export}:${e.global}`)
|
||||
.join(",");
|
||||
output += `define("${moduleName}", () => {return {${rawExports}}});\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "rfc176-shims",
|
||||
"keywords": [
|
||||
"ember-addon"
|
||||
]
|
||||
}
|
||||
@ -49,6 +49,7 @@
|
||||
"ember-load-initializers": "^2.1.1",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-qunit": "^5.1.2",
|
||||
"ember-rfc176-data": "^0.3.17",
|
||||
"ember-source": "~3.15.0",
|
||||
"ember-test-selectors": "^6.0.0",
|
||||
"eslint": "^7.27.0",
|
||||
@ -78,7 +79,8 @@
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
"lib/bootstrap-json"
|
||||
"lib/bootstrap-json",
|
||||
"lib/rfc176-shims"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -2,157 +2,6 @@
|
||||
if (window.unsupportedBrowser) {
|
||||
throw "Unsupported browser detected";
|
||||
}
|
||||
// TODO: These are needed to load plugins because @ember has its own loader.
|
||||
// We should find a nicer way to do this.
|
||||
const EMBER_MODULES = {
|
||||
"@ember/application": {
|
||||
default: Ember.Application,
|
||||
setOwner: Ember.setOwner,
|
||||
getOwner: Ember.getOwner,
|
||||
},
|
||||
"@ember/array": {
|
||||
default: Ember.Array,
|
||||
A: Ember.A,
|
||||
isArray: Ember.isArray,
|
||||
},
|
||||
"@ember/array/proxy": {
|
||||
default: Ember.ArrayProxy,
|
||||
},
|
||||
"@ember/component": {
|
||||
default: Ember.Component,
|
||||
},
|
||||
"@ember/component/helper": {
|
||||
default: Ember.Helper,
|
||||
},
|
||||
"@ember/component/text-field": {
|
||||
default: Ember.TextField,
|
||||
},
|
||||
"@ember/component/text-area": {
|
||||
default: Ember.TextArea,
|
||||
},
|
||||
"@ember/controller": {
|
||||
default: Ember.Controller,
|
||||
inject: Ember.inject.controller,
|
||||
},
|
||||
"@ember/debug": {
|
||||
warn: Ember.warn,
|
||||
},
|
||||
"@ember/error": {
|
||||
default: Ember.error,
|
||||
},
|
||||
"@ember/object": {
|
||||
action: Ember._action,
|
||||
default: Ember.Object,
|
||||
get: Ember.get,
|
||||
getProperties: Ember.getProperties,
|
||||
set: Ember.set,
|
||||
setProperties: Ember.setProperties,
|
||||
computed: Ember.computed,
|
||||
defineProperty: Ember.defineProperty,
|
||||
},
|
||||
"@ember/object/computed": {
|
||||
alias: Ember.computed.alias,
|
||||
and: Ember.computed.and,
|
||||
bool: Ember.computed.bool,
|
||||
collect: Ember.computed.collect,
|
||||
deprecatingAlias: Ember.computed.deprecatingAlias,
|
||||
empty: Ember.computed.empty,
|
||||
equal: Ember.computed.equal,
|
||||
filter: Ember.computed.filter,
|
||||
filterBy: Ember.computed.filterBy,
|
||||
gt: Ember.computed.gt,
|
||||
gte: Ember.computed.gte,
|
||||
intersect: Ember.computed.intersect,
|
||||
lt: Ember.computed.lt,
|
||||
lte: Ember.computed.lte,
|
||||
map: Ember.computed.map,
|
||||
mapBy: Ember.computed.mapBy,
|
||||
match: Ember.computed.match,
|
||||
max: Ember.computed.max,
|
||||
min: Ember.computed.min,
|
||||
none: Ember.computed.none,
|
||||
not: Ember.computed.not,
|
||||
notEmpty: Ember.computed.notEmpty,
|
||||
oneWay: Ember.computed.oneWay,
|
||||
or: Ember.computed.or,
|
||||
readOnly: Ember.computed.readOnly,
|
||||
reads: Ember.computed.reads,
|
||||
setDiff: Ember.computed.setDiff,
|
||||
sort: Ember.computed.sort,
|
||||
sum: Ember.computed.sum,
|
||||
union: Ember.computed.union,
|
||||
uniq: Ember.computed.uniq,
|
||||
uniqBy: Ember.computed.uniqBy,
|
||||
},
|
||||
"@ember/object/internals": {
|
||||
guidFor: Ember.guidFor,
|
||||
},
|
||||
"@ember/object/mixin": { default: Ember.Mixin },
|
||||
"@ember/object/proxy": { default: Ember.ObjectProxy },
|
||||
"@ember/object/promise-proxy-mixin": { default: Ember.PromiseProxyMixin },
|
||||
"@ember/object/evented": {
|
||||
default: Ember.Evented,
|
||||
on: Ember.on,
|
||||
},
|
||||
"@ember/routing/route": { default: Ember.Route },
|
||||
"@ember/routing/router": { default: Ember.Router },
|
||||
"@ember/runloop": {
|
||||
bind: Ember.run.bind,
|
||||
cancel: Ember.run.cancel,
|
||||
debounce: Ember.testing ? Ember.run : Ember.run.debounce,
|
||||
later: Ember.run.later,
|
||||
next: Ember.run.next,
|
||||
once: Ember.run.once,
|
||||
run: Ember.run,
|
||||
schedule: Ember.run.schedule,
|
||||
scheduleOnce: Ember.run.scheduleOnce,
|
||||
throttle: Ember.run.throttle,
|
||||
},
|
||||
"@ember/service": {
|
||||
default: Ember.Service,
|
||||
inject: Ember.inject.service,
|
||||
},
|
||||
"@ember/string": {
|
||||
w: Ember.String.w,
|
||||
dasherize: Ember.String.dasherize,
|
||||
decamelize: Ember.String.decamelize,
|
||||
camelize: Ember.String.camelize,
|
||||
classify: Ember.String.classify,
|
||||
underscore: Ember.String.underscore,
|
||||
capitalize: Ember.String.capitalize,
|
||||
},
|
||||
"@ember/template": {
|
||||
htmlSafe: Ember.String.htmlSafe,
|
||||
},
|
||||
"@ember/utils": {
|
||||
isBlank: Ember.isBlank,
|
||||
isEmpty: Ember.isEmpty,
|
||||
isNone: Ember.isNone,
|
||||
isPresent: Ember.isPresent,
|
||||
},
|
||||
jquery: { default: $ },
|
||||
rsvp: {
|
||||
asap: Ember.RSVP.asap,
|
||||
all: Ember.RSVP.all,
|
||||
allSettled: Ember.RSVP.allSettled,
|
||||
race: Ember.RSVP.race,
|
||||
hash: Ember.RSVP.hash,
|
||||
hashSettled: Ember.RSVP.hashSettled,
|
||||
rethrow: Ember.RSVP.rethrow,
|
||||
defer: Ember.RSVP.defer,
|
||||
denodeify: Ember.RSVP.denodeify,
|
||||
resolve: Ember.RSVP.resolve,
|
||||
reject: Ember.RSVP.reject,
|
||||
map: Ember.RSVP.map,
|
||||
filter: Ember.RSVP.filter,
|
||||
default: Ember.RSVP,
|
||||
Promise: Ember.RSVP.Promise,
|
||||
EventTarget: Ember.RSVP.EventTarget,
|
||||
},
|
||||
};
|
||||
Object.keys(EMBER_MODULES).forEach((mod) => {
|
||||
define(mod, () => EMBER_MODULES[mod]);
|
||||
});
|
||||
|
||||
// TODO: Remove this and have resolver find the templates
|
||||
const prefix = "discourse/templates/";
|
||||
@ -166,15 +15,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
define("I18n", ["exports"], function (exports) {
|
||||
return I18n;
|
||||
});
|
||||
|
||||
define("htmlbars-inline-precompile", ["exports"], function (exports) {
|
||||
exports.default = function tag(strings) {
|
||||
return Ember.Handlebars.compile(strings[0]);
|
||||
};
|
||||
});
|
||||
window.__widget_helpers = require("discourse-widget-hbs/helpers").default;
|
||||
|
||||
// TODO: Eliminate this global
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
define("I18n", ["exports"], function (exports) {
|
||||
return I18n;
|
||||
});
|
||||
|
||||
define("htmlbars-inline-precompile", ["exports"], function (exports) {
|
||||
exports.default = function tag(strings) {
|
||||
return Ember.Handlebars.compile(strings[0]);
|
||||
};
|
||||
});
|
||||
@ -13,6 +13,9 @@ import {
|
||||
} from "@ember/test-helpers";
|
||||
import siteSettingFixture from "discourse/tests/fixtures/site-settings";
|
||||
import { test } from "qunit";
|
||||
import pretender from "discourse/tests/helpers/create-pretender";
|
||||
|
||||
const ENTER_KEYCODE = 13;
|
||||
|
||||
acceptance("Admin - Site Settings", function (needs) {
|
||||
let updatedTitle;
|
||||
@ -105,7 +108,7 @@ acceptance("Admin - Site Settings", function (needs) {
|
||||
);
|
||||
|
||||
await fillIn(".input-setting-string", "Test");
|
||||
await triggerKeyEvent(".input-setting-string", "keydown", 13); // enter
|
||||
await triggerKeyEvent(".input-setting-string", "keydown", ENTER_KEYCODE);
|
||||
assert.ok(
|
||||
exists(".row.setting.overridden"),
|
||||
"saving via Enter key marks setting as overriden"
|
||||
@ -163,4 +166,30 @@ acceptance("Admin - Site Settings", function (needs) {
|
||||
"/admin/site_settings/category/all_results?filter=contact"
|
||||
);
|
||||
});
|
||||
|
||||
test("filters * and ? for domain lists", async (assert) => {
|
||||
pretender.put("/admin/site_settings/blocked_onebox_domains", () => [200]);
|
||||
|
||||
await visit("/admin/site_settings");
|
||||
await fillIn("#setting-filter", "domains");
|
||||
|
||||
await click(".select-kit-header.multi-select-header");
|
||||
|
||||
await fillIn(".select-kit-filter input", "cat.?.domain");
|
||||
await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE);
|
||||
|
||||
await fillIn(".select-kit-filter input", "*.domain");
|
||||
await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE);
|
||||
|
||||
await fillIn(".select-kit-filter input", "proper.com");
|
||||
await triggerKeyEvent(".select-kit-filter input", "keydown", ENTER_KEYCODE);
|
||||
|
||||
await click("button.ok");
|
||||
|
||||
assert.strictEqual(
|
||||
pretender.handledRequests[pretender.handledRequests.length - 1]
|
||||
.requestBody,
|
||||
"blocked_onebox_domains=proper.com"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import I18n from "I18n";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { skip } from "qunit";
|
||||
import { test } from "qunit";
|
||||
import topicFixtures from "discourse/tests/fixtures/topic";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
|
||||
@ -104,7 +104,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
server.get("/t/280.json", () => helper.response(topicResponse));
|
||||
});
|
||||
|
||||
skip("Bookmarks modal opening", async function (assert) {
|
||||
test("Bookmarks modal opening", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
assert.ok(
|
||||
@ -113,7 +113,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Bookmarks modal selecting reminder type", async function (assert) {
|
||||
test("Bookmarks modal selecting reminder type", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await openBookmarkModal();
|
||||
@ -133,7 +133,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
await click("#save-bookmark");
|
||||
});
|
||||
|
||||
skip("Saving a bookmark with a reminder", async function (assert) {
|
||||
test("Saving a bookmark with a reminder", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
await fillIn("input#bookmark-name", "Check this out later");
|
||||
@ -151,7 +151,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Opening the options panel and remembering the option", async function (assert) {
|
||||
test("Opening the options panel and remembering the option", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
await click(".bookmark-options-button");
|
||||
@ -174,7 +174,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Saving a bookmark with no reminder or name", async function (assert) {
|
||||
test("Saving a bookmark with no reminder or name", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
await click("#save-bookmark");
|
||||
@ -183,7 +183,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
||||
"it shows the bookmarked icon on the post"
|
||||
);
|
||||
assert.not(
|
||||
assert.notOk(
|
||||
exists(
|
||||
".topic-post:first-child button.bookmark.bookmarked > .d-icon-discourse-bookmark-clock"
|
||||
),
|
||||
@ -191,7 +191,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Deleting a bookmark with a reminder", async function (assert) {
|
||||
test("Deleting a bookmark with a reminder", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
await click("#tap_tile_tomorrow");
|
||||
@ -215,23 +215,23 @@ acceptance("Bookmarking", function (needs) {
|
||||
|
||||
await click(".bootbox.modal .btn-primary");
|
||||
|
||||
assert.not(
|
||||
assert.notOk(
|
||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
||||
"it no longer shows the bookmarked icon on the post after bookmark is deleted"
|
||||
);
|
||||
});
|
||||
|
||||
skip("Cancelling saving a bookmark", async function (assert) {
|
||||
test("Cancelling saving a bookmark", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal();
|
||||
await click(".d-modal-cancel");
|
||||
assert.not(
|
||||
assert.notOk(
|
||||
exists(".topic-post:first-child button.bookmark.bookmarked"),
|
||||
"it does not show the bookmarked icon on the post because it is not saved"
|
||||
);
|
||||
});
|
||||
|
||||
skip("Editing a bookmark", async function (assert) {
|
||||
test("Editing a bookmark", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
let now = moment.tz(loggedInUser().resolvedTimezone(loggedInUser()));
|
||||
let tomorrow = now.add(1, "day").format("YYYY-MM-DD");
|
||||
@ -257,7 +257,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Using a post date for the reminder date", async function (assert) {
|
||||
test("Using a post date for the reminder date", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
let postDate = moment.tz(
|
||||
"2036-01-15",
|
||||
@ -286,7 +286,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Cannot use the post date for a reminder when the post date is in the past", async function (assert) {
|
||||
test("Cannot use the post date for a reminder when the post date is in the past", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal(2);
|
||||
assert.notOk(
|
||||
@ -295,7 +295,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("The topic level bookmark button deletes all bookmarks if several posts on the topic are bookmarked", async function (assert) {
|
||||
test("The topic level bookmark button deletes all bookmarks if several posts on the topic are bookmarked", async function (assert) {
|
||||
const yesButton = "a.btn-primary";
|
||||
const noButton = "a.btn-default";
|
||||
|
||||
@ -341,7 +341,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("The topic level bookmark button opens the edit modal if only the first post on the topic is bookmarked", async function (assert) {
|
||||
test("The topic level bookmark button opens the edit modal if only the first post on the topic is bookmarked", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal(1);
|
||||
await click("#save-bookmark");
|
||||
@ -360,7 +360,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Creating and editing a topic level bookmark", async function (assert) {
|
||||
test("Creating and editing a topic level bookmark", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-button-bookmark");
|
||||
|
||||
@ -432,7 +432,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Deleting a topic_level bookmark with a reminder", async function (assert) {
|
||||
test("Deleting a topic_level bookmark with a reminder", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-button-bookmark");
|
||||
await click("#save-bookmark");
|
||||
@ -467,7 +467,7 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("The topic level bookmark button opens the edit modal if only one post in the post stream is bookmarked", async function (assert) {
|
||||
test("The topic level bookmark button opens the edit modal if only one post in the post stream is bookmarked", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openBookmarkModal(2);
|
||||
await click("#save-bookmark");
|
||||
@ -486,12 +486,12 @@ acceptance("Bookmarking", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the first post", async function (assert) {
|
||||
test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the first post", async function (assert) {
|
||||
const postNumber = 1;
|
||||
await testTopicLevelBookmarkButtonIcon(assert, postNumber);
|
||||
});
|
||||
|
||||
skip("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the second post", async function (assert) {
|
||||
test("The topic level bookmark button shows an icon with a clock if there is a bookmark with a reminder on the second post", async function (assert) {
|
||||
const postNumber = 2;
|
||||
await testTopicLevelBookmarkButtonIcon(assert, postNumber);
|
||||
});
|
||||
|
||||
@ -4,8 +4,7 @@ import {
|
||||
exists,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||
import { skip } from "qunit";
|
||||
// import { test } from "qunit";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Click Track", function (needs) {
|
||||
let tracked = false;
|
||||
@ -16,7 +15,7 @@ acceptance("Click Track", function (needs) {
|
||||
});
|
||||
});
|
||||
|
||||
skip("Do not track mentions", async function (assert) {
|
||||
test("Do not track mentions", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
assert.ok(!exists(".user-card.show"), "card should not appear");
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import { Promise } from "rsvp";
|
||||
import { _clearSnapshots } from "select-kit/components/composer-actions";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import sinon from "sinon";
|
||||
import { skip, test } from "qunit";
|
||||
import { test } from "qunit";
|
||||
import { toggleCheckDraftPopup } from "discourse/controllers/composer";
|
||||
|
||||
acceptance("Composer Actions", function (needs) {
|
||||
@ -35,7 +35,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
assert.ok(queryAll(".d-editor-input").val(), "this is the reply");
|
||||
});
|
||||
|
||||
skip("replying to post", async function (assert) {
|
||||
test("replying to post", async function (assert) {
|
||||
const composerActions = selectKit(".composer-actions");
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
@ -76,7 +76,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("replying to post - reply_to_topic", async function (assert) {
|
||||
test("replying to post - reply_to_topic", async function (assert) {
|
||||
const composerActions = selectKit(".composer-actions");
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
@ -103,7 +103,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("replying to post - toggle_whisper", async function (assert) {
|
||||
test("replying to post - toggle_whisper", async function (assert) {
|
||||
const composerActions = selectKit(".composer-actions");
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
@ -405,7 +405,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("replying to post as TL3 user", async function (assert) {
|
||||
test("replying to post as TL3 user", async function (assert) {
|
||||
const composerActions = selectKit(".composer-actions");
|
||||
|
||||
updateCurrentUser({ moderator: false, admin: false, trust_level: 3 });
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import I18n from "I18n";
|
||||
import { skip, test } from "qunit";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Composer - Edit conflict", function (needs) {
|
||||
needs.user();
|
||||
@ -14,24 +13,6 @@ acceptance("Composer - Edit conflict", function (needs) {
|
||||
});
|
||||
});
|
||||
|
||||
skip("Edit a post that causes an edit conflict", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:nth-of-type(1) button.show-more-actions");
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
await fillIn(".d-editor-input", "this will 409");
|
||||
await click("#reply-control button.create");
|
||||
assert.strictEqual(
|
||||
queryAll("#reply-control button.create").text().trim(),
|
||||
I18n.t("composer.overwrite_edit"),
|
||||
"it shows the overwrite button"
|
||||
);
|
||||
assert.ok(
|
||||
queryAll("#draft-status .d-icon-user-edit"),
|
||||
"error icon should be there"
|
||||
);
|
||||
await click(".modal .btn-primary");
|
||||
});
|
||||
|
||||
test("Should not send originalText when posting a new reply", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:nth-of-type(1) button.reply");
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { run } from "@ember/runloop";
|
||||
import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
|
||||
import { click, currentURL, fillIn, settled, visit } from "@ember/test-helpers";
|
||||
import { toggleCheckDraftPopup } from "discourse/controllers/composer";
|
||||
import LinkLookup from "discourse/lib/link-lookup";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { CREATE_TOPIC, NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||
import Composer, {
|
||||
CREATE_TOPIC,
|
||||
NEW_TOPIC_KEY,
|
||||
} from "discourse/models/composer";
|
||||
import Draft from "discourse/models/draft";
|
||||
import {
|
||||
acceptance,
|
||||
@ -43,7 +46,7 @@ acceptance("Composer", function (needs) {
|
||||
});
|
||||
});
|
||||
|
||||
skip("Tests the Composer controls", async function (assert) {
|
||||
test("Tests the Composer controls", async function (assert) {
|
||||
await visit("/");
|
||||
assert.ok(exists("#create-topic"), "the create button is visible");
|
||||
|
||||
@ -58,13 +61,13 @@ acceptance("Composer", function (needs) {
|
||||
"body errors are hidden by default"
|
||||
);
|
||||
|
||||
await click("a.toggle-preview");
|
||||
await click(".toggle-preview");
|
||||
assert.ok(
|
||||
!exists(".d-editor-preview:visible"),
|
||||
"clicking the toggle hides the preview"
|
||||
);
|
||||
|
||||
await click("a.toggle-preview");
|
||||
await click(".toggle-preview");
|
||||
assert.ok(
|
||||
exists(".d-editor-preview:visible"),
|
||||
"clicking the toggle shows the preview again"
|
||||
@ -116,9 +119,9 @@ acceptance("Composer", function (needs) {
|
||||
);
|
||||
|
||||
await click("#reply-control a.cancel");
|
||||
assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog");
|
||||
assert.ok(exists(".d-modal"), "it pops up a confirmation dialog");
|
||||
|
||||
await click(".modal-footer a:nth-of-type(2)");
|
||||
await click(".modal-footer .discard-draft");
|
||||
assert.ok(!exists(".bootbox.modal"), "the confirmation can be cancelled");
|
||||
});
|
||||
|
||||
@ -234,7 +237,7 @@ acceptance("Composer", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Posting on a different topic", async function (assert) {
|
||||
test("Posting on a different topic", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-buttons .btn.create");
|
||||
await fillIn(
|
||||
@ -386,24 +389,6 @@ acceptance("Composer", function (needs) {
|
||||
assert.strictEqual(count(".topic-post.staged"), 0);
|
||||
});
|
||||
|
||||
skip("Editing a post can rollback to old content", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".topic-post:nth-of-type(1) button.show-more-actions");
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
|
||||
await fillIn(".d-editor-input", "this will 409");
|
||||
await fillIn("#reply-title", "This is the new text for the title");
|
||||
await click("#reply-control button.create");
|
||||
|
||||
assert.ok(!exists(".topic-post.staged"));
|
||||
assert.strictEqual(
|
||||
query(".topic-post .cooked").innerText,
|
||||
"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?"
|
||||
);
|
||||
|
||||
await click(".bootbox.modal .btn-primary");
|
||||
});
|
||||
|
||||
test("Composer can switch between edits", async function (assert) {
|
||||
await visit("/t/this-is-a-test-topic/9");
|
||||
|
||||
@ -686,7 +671,7 @@ acceptance("Composer", function (needs) {
|
||||
}
|
||||
});
|
||||
|
||||
skip("Can switch states without abandon popup", async function (assert) {
|
||||
test("Can switch states without abandon popup", async function (assert) {
|
||||
try {
|
||||
toggleCheckDraftPopup(true);
|
||||
|
||||
@ -834,7 +819,7 @@ acceptance("Composer", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
skip("Shows duplicate_link notice", async function (assert) {
|
||||
test("Shows duplicate_link notice", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#topic-footer-buttons .create");
|
||||
|
||||
@ -936,3 +921,101 @@ acceptance("Composer - Customizations", function (needs) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// all of these are broken on legacy ember qunit for...some reason. commenting
|
||||
// until we are fully on ember cli.
|
||||
acceptance("Composer - Focus Open and Closed", function (needs) {
|
||||
needs.user();
|
||||
|
||||
skip("Focusing a composer which is not open with create topic", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
composer.focusComposer({ fallbackToNewTopic: true });
|
||||
|
||||
await settled();
|
||||
assert.strictEqual(
|
||||
document.activeElement.classList.contains("d-editor-input"),
|
||||
true,
|
||||
"composer is opened and focused"
|
||||
);
|
||||
assert.strictEqual(composer.model.action, Composer.CREATE_TOPIC);
|
||||
});
|
||||
|
||||
skip("Focusing a composer which is not open with create topic and append text", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
composer.focusComposer({
|
||||
fallbackToNewTopic: true,
|
||||
insertText: "this is appended",
|
||||
});
|
||||
|
||||
await settled();
|
||||
assert.strictEqual(
|
||||
document.activeElement.classList.contains("d-editor-input"),
|
||||
true,
|
||||
"composer is opened and focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query("textarea.d-editor-input").value.trim(),
|
||||
"this is appended"
|
||||
);
|
||||
});
|
||||
|
||||
skip("Focusing a composer which is already open", async function (assert) {
|
||||
await visit("/");
|
||||
await click("#create-topic");
|
||||
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
composer.focusComposer();
|
||||
|
||||
await settled();
|
||||
assert.strictEqual(
|
||||
document.activeElement.classList.contains("d-editor-input"),
|
||||
true,
|
||||
"composer is opened and focused"
|
||||
);
|
||||
});
|
||||
|
||||
skip("Focusing a composer which is already open and append text", async function (assert) {
|
||||
await visit("/");
|
||||
await click("#create-topic");
|
||||
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
composer.focusComposer({ insertText: "this is some appended text" });
|
||||
|
||||
await settled();
|
||||
assert.strictEqual(
|
||||
document.activeElement.classList.contains("d-editor-input"),
|
||||
true,
|
||||
"composer is opened and focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query("textarea.d-editor-input").value.trim(),
|
||||
"this is some appended text"
|
||||
);
|
||||
});
|
||||
|
||||
skip("Focusing a composer which is not open that has a draft", async function (assert) {
|
||||
await visit("/t/this-is-a-test-topic/9");
|
||||
|
||||
await click(".topic-post:nth-of-type(1) button.edit");
|
||||
await fillIn(".d-editor-input", "This is a dirty reply");
|
||||
await click(".toggle-minimize");
|
||||
|
||||
const composer = this.container.lookup("controller:composer");
|
||||
composer.focusComposer({ insertText: "this is some appended text" });
|
||||
|
||||
await settled();
|
||||
assert.strictEqual(
|
||||
document.activeElement.classList.contains("d-editor-input"),
|
||||
true,
|
||||
"composer is opened and focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query("textarea.d-editor-input").value.trim(),
|
||||
"This is a dirty reply\n\nthis is some appended text"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -72,18 +72,21 @@ acceptance(
|
||||
id: 1,
|
||||
name: "test won",
|
||||
slug: "test-won",
|
||||
permission: 1,
|
||||
topic_template: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "test too",
|
||||
slug: "test-too",
|
||||
permission: 1,
|
||||
topic_template: "",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "test free",
|
||||
slug: "test-free",
|
||||
permission: 1,
|
||||
topic_template: null,
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user