Version bump

This commit is contained in:
Krzysztof Kotlarek 2022-02-14 15:52:34 +11:00
commit 1a46b092fc
461 changed files with 7052 additions and 5304 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { htmlHelper } from "discourse-common/lib/helpers";
export default htmlHelper((color) => `--category-color: #${color};`);

View File

@ -36,5 +36,6 @@ export function autoLoadModules(container, registry) {
export default {
name: "auto-load-modules",
after: "inject-objects",
initialize: (container) => autoLoadModules(container, container.registry),
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ export default Mixin.create({
bulkSelectEnabled: false,
autoAddTopicsToBulkSelect: false,
selected: null,
lastChecked: null,
canBulkSelect: or("currentUser.staff", "showDismissRead", "showResetNew"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,6 @@
customOptions=customTimeShortcutOptions
hiddenOptions=hiddenTimeShortcutOptions
customLabels=customTimeShortcutLabels
additionalOptionsToShow=additionalTimeShortcutOptions
_itsatrap=_itsatrap
}}
{{else}}

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
placeholderKey=composer.titlePlaceholder
aria-label=(I18n composer.titlePlaceholder)
disabled=disabled
autocomplete="discourse"
autocomplete="off"
}}
{{popup-input-tip validation=validation}}

View File

@ -43,7 +43,7 @@
{{conditional-loading-spinner condition=loading}}
{{d-textarea
autocomplete="discourse"
autocomplete="off"
tabindex=tabindex
value=value
class="d-editor-input"

View File

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

View File

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

View File

@ -9,7 +9,6 @@
includeDateTime=includeDateTime
includeWeekend=includeWeekend
includeFarFuture=includeFarFuture
includeMidFuture=includeMidFuture
includeNow=includeNow
clearable=clearable
onChangeInput=onChangeInput

View File

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

View File

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

View File

@ -35,7 +35,6 @@
</label>
{{future-date-input
includeDateTime=true
includeMidFuture=true
clearable=true
onChangeInput=(action (mut inviteExpiresAt))
}}

View File

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

View File

@ -1 +1,6 @@
{{topicListItemContents}}
{{plugin-outlet
name="after-topic-list-item"
args=(hash topic=topic)
}}

View File

@ -41,6 +41,7 @@
expandAllPinned=expandAllPinned
lastVisitedTopic=lastVisitedTopic
selected=selected
lastChecked=lastChecked
tagsForUser=tagsForUser}}
{{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}}
{{/each}}

View File

@ -4,7 +4,7 @@
{{text-field
value=filterInput
placeholderKey=filterPlaceholder
autocomplete="discourse"
autocomplete="off"
class="group-username-filter no-blur"
}}
{{/if}}

View File

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

View File

@ -2,7 +2,7 @@
{{text-field
value=filterInput
placeholderKey=filterPlaceholder
autocomplete="discourse"
autocomplete="off"
class="group-username-filter no-blur"
}}

View File

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

View File

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

View File

@ -15,7 +15,6 @@
input=(readonly ignoredUntil)
includeWeekend=true
includeDateTime=false
includeMidFuture=true
includeFarFuture=true
onChangeInput=(action (mut ignoredUntil))
}}

View File

@ -4,7 +4,6 @@
input=ignoredUntil
includeWeekend=true
includeDateTime=false
includeMidFuture=true
includeFarFuture=true
onChangeInput=(action (mut ignoredUntil))
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
engine-strict = true

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

View File

@ -0,0 +1,6 @@
{
"name": "rfc176-shims",
"keywords": [
"ember-addon"
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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