Version bump

This commit is contained in:
Neil Lalonde 2021-04-29 13:40:46 -04:00
commit 992587f836
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
476 changed files with 30312 additions and 3246 deletions

2
.tool-versions Normal file
View File

@ -0,0 +1,2 @@
nodejs 15.14.0
ruby 2.7.2

14
Gemfile
View File

@ -18,13 +18,13 @@ else
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
gem 'actionmailer', '6.0.3.5'
gem 'actionpack', '6.0.3.5'
gem 'actionview', '6.0.3.5'
gem 'activemodel', '6.0.3.5'
gem 'activerecord', '6.0.3.5'
gem 'activesupport', '6.0.3.5'
gem 'railties', '6.0.3.5'
gem 'actionmailer', '6.1.3.1'
gem 'actionpack', '6.1.3.1'
gem 'actionview', '6.1.3.1'
gem 'activemodel', '6.1.3.1'
gem 'activerecord', '6.1.3.1'
gem 'activesupport', '6.1.3.1'
gem 'railties', '6.1.3.1'
gem 'sprockets-rails'
end

View File

@ -8,21 +8,22 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actionmailer (6.0.3.5)
actionpack (= 6.0.3.5)
actionview (= 6.0.3.5)
activejob (= 6.0.3.5)
actionmailer (6.1.3.1)
actionpack (= 6.1.3.1)
actionview (= 6.1.3.1)
activejob (= 6.1.3.1)
activesupport (= 6.1.3.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.5)
actionview (= 6.0.3.5)
activesupport (= 6.0.3.5)
rack (~> 2.0, >= 2.0.8)
actionpack (6.1.3.1)
actionview (= 6.1.3.1)
activesupport (= 6.1.3.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (6.0.3.5)
activesupport (= 6.0.3.5)
actionview (6.1.3.1)
activesupport (= 6.1.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -31,20 +32,20 @@ GEM
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (6.0.3.5)
activesupport (= 6.0.3.5)
activejob (6.1.3.1)
activesupport (= 6.1.3.1)
globalid (>= 0.3.6)
activemodel (6.0.3.5)
activesupport (= 6.0.3.5)
activerecord (6.0.3.5)
activemodel (= 6.0.3.5)
activesupport (= 6.0.3.5)
activesupport (6.0.3.5)
activemodel (6.1.3.1)
activesupport (= 6.1.3.1)
activerecord (6.1.3.1)
activemodel (= 6.1.3.1)
activesupport (= 6.1.3.1)
activesupport (6.1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.1.1)
@ -79,7 +80,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.7.3)
bootsnap (1.7.4)
msgpack (~> 1.0)
builder (3.2.4)
bullet (6.1.4)
@ -92,7 +93,7 @@ GEM
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.1.8)
connection_pool (2.2.4)
connection_pool (2.2.5)
cose (1.2.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -114,7 +115,7 @@ GEM
railties (>= 3.1)
discourse-ember-source (3.12.2.3)
discourse-fonts (0.0.8)
discourse_dev (0.0.9)
discourse_dev (0.1.0)
faker (~> 2.16)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
@ -133,18 +134,22 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
excon (0.79.0)
excon (0.81.0)
execjs (2.7.0)
exifr (1.3.9)
fabrication (2.22.0)
faker (2.17.0)
i18n (>= 1.6, < 2)
fakeweb (1.3.0)
faraday (1.3.0)
faraday (1.4.1)
faraday-excon (~> 1.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
multipart-post (>= 1.2, < 3)
ruby2_keywords
ruby2_keywords (>= 0.0.4)
faraday-excon (1.1.0)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
fast_blank (1.0.0)
fast_xs (0.8.0)
fastimage (2.2.3)
@ -178,14 +183,14 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.2.2)
jwt (2.2.3)
kgio (2.11.3)
libv8-node (15.14.0.0)
libv8-node (15.14.0.0-arm64-darwin-20)
libv8-node (15.14.0.0-x86_64-darwin-18)
libv8-node (15.14.0.0-x86_64-darwin-19)
libv8-node (15.14.0.0-x86_64-darwin-20)
libv8-node (15.14.0.0-x86_64-linux)
libv8-node (15.14.0.1)
libv8-node (15.14.0.1-arm64-darwin-20)
libv8-node (15.14.0.1-x86_64-darwin-18)
libv8-node (15.14.0.1-x86_64-darwin-19)
libv8-node (15.14.0.1-x86_64-darwin-20)
libv8-node (15.14.0.1-x86_64-linux)
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@ -205,11 +210,11 @@ GEM
lz4-ruby (0.3.3)
maxminddb (0.1.22)
memory_profiler (1.0.0)
message_bus (3.3.4)
message_bus (3.3.5)
rack (>= 1.1.3)
method_source (1.0.0)
mini_mime (1.1.0)
mini_portile2 (2.5.0)
mini_portile2 (2.5.1)
mini_racer (0.4.0)
libv8-node (~> 15.14.0.0)
mini_scheduler (0.13.0)
@ -245,7 +250,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.11.3)
oj (3.11.5)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -268,7 +273,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (2.2.12)
onebox (2.2.14)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
@ -311,19 +316,19 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_failover (0.6.5)
rails_failover (0.7.3)
activerecord (~> 6.0)
concurrent-ruby
railties (~> 6.0)
rails_multisite (2.5.0)
rails_multisite (3.0.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.0.3.5)
actionpack (= 6.0.3.5)
activesupport (= 6.0.3.5)
railties (6.1.3.1)
actionpack (= 6.1.3.1)
activesupport (= 6.1.3.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
thor (~> 1.0)
rainbow (3.0.0)
raindrops (0.19.1)
rake (13.0.3)
@ -377,7 +382,7 @@ GEM
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (1.12.1)
rubocop (1.13.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -391,7 +396,7 @@ GEM
rubocop-discourse (2.4.1)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.2.0)
rubocop-rspec (2.3.0)
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
ruby-prof (1.4.3)
@ -440,10 +445,9 @@ GEM
stackprof (0.2.16)
test-prof (1.0.2)
thor (1.1.0)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.9)
thread_safe (~> 0.1)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
@ -475,14 +479,14 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
actionmailer (= 6.0.3.5)
actionpack (= 6.0.3.5)
actionview (= 6.0.3.5)
actionmailer (= 6.1.3.1)
actionpack (= 6.1.3.1)
actionview (= 6.1.3.1)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 6.0.3.5)
activerecord (= 6.0.3.5)
activesupport (= 6.0.3.5)
activemodel (= 6.1.3.1)
activerecord (= 6.1.3.1)
activesupport (= 6.1.3.1)
addressable
annotate
aws-sdk-s3
@ -562,7 +566,7 @@ DEPENDENCIES
rack-protection
rails_failover
rails_multisite
railties (= 6.0.3.5)
railties (= 6.1.3.1)
rake
rb-fsevent
rbtrace

View File

@ -7,7 +7,3 @@
require File.expand_path('../config/application', __FILE__)
Discourse::Application.load_tasks
# this prevents crashes when migrating a database in production in certain
# PostgreSQL configuations when trying to create structure.sql
Rake::Task["db:structure:dump"].clear if Rails.env.production?

View File

@ -68,6 +68,8 @@ export default Component.extend({
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
endDate: null,
startDate: null,
init() {
this._super(...arguments);
@ -82,25 +84,21 @@ export default Component.extend({
.includes(this.dataSourceName);
}),
startDate: computed("filters.startDate", function () {
if (this.filters && isPresent(this.filters.startDate)) {
return moment(this.filters.startDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
endDate: computed("filters.endDate", function () {
if (this.filters && isPresent(this.filters.endDate)) {
return moment(this.filters.endDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
didReceiveAttrs() {
this._super(...arguments);
let startDate = moment();
if (this.filters && isPresent(this.filters.startDate)) {
startDate = moment(this.filters.startDate, "YYYY-MM-DD");
}
this.set("startDate", startDate);
let endDate = moment();
if (this.filters && isPresent(this.filters.endDate)) {
endDate = moment(this.filters.endDate, "YYYY-MM-DD");
}
this.set("endDate", endDate);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
@ -213,7 +211,7 @@ export default Component.extend({
@action
onChangeDateRange(range) {
this.send("refreshReport", {
this.setProperties({
startDate: range.from,
endDate: range.to,
});

View File

@ -44,25 +44,25 @@ export default Component.extend(bufferedProperty("userField"), {
},
@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
"userField.{editable,required,show_on_profile,show_on_user_card,searchable}"
)
flags(editable, required, showOnProfile, showOnUserCard) {
flags(userField) {
const ret = [];
if (editable) {
if (userField.editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
if (userField.required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
if (userField.showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
if (userField.showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
if (userField.searchable) {
ret.push(I18n.t("admin.user_fields.searchable.enabled"));
}
return ret.join(", ");
},
@ -78,6 +78,7 @@ export default Component.extend(bufferedProperty("userField"), {
"required",
"show_on_profile",
"show_on_user_card",
"searchable",
"options"
);

View File

@ -24,7 +24,7 @@ export default Controller.extend(ModalFunctionality, {
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: match("uploadUrl", /^.*[@].*[:].*\.git$/),
checkPrivate: match("uploadUrl", /^ssh\:\/\/.*\@.*\.git$|.*\@.*\:.*\.git$/),
localFile: null,
uploadUrl: null,
uploadName: null,

View File

@ -278,6 +278,7 @@ const Report = EmberObject.extend({
return {
title: label.title,
htmlTitle: label.html_title,
sortProperty: label.sort_property || mainProperty,
mainProperty,
type,

View File

@ -1,6 +1,10 @@
import Route from "@ember/routing/route";
export default Route.extend({
beforeModel() {
this.transitionTo("adminCustomizeThemes");
if (this.currentUser.admin) {
this.transitionTo("adminCustomizeThemes");
} else {
this.transitionTo("adminWatchedWords");
}
},
});

View File

@ -18,8 +18,8 @@
{{nav-item route="adminEmail" label="admin.email.title"}}
{{/if}}
{{nav-item route="adminLogs" label="admin.logs.title"}}
{{nav-item route="adminCustomize" label="admin.customize.title"}}
{{#if currentUser.admin}}
{{nav-item route="adminCustomize" label="admin.customize.title"}}
{{nav-item route="adminApi" label="admin.api.title"}}
{{#if siteSettings.enable_backups}}
{{nav-item route="admin.backups" label="admin.backups.title"}}

View File

@ -2,4 +2,8 @@
{{d-button action=sortByLabel icon=sortIcon class="sort-btn"}}
{{/if}}
<span class="title">{{label.title}}</span>
{{#if label.htmlTitle}}
<span class="title">{{html-safe label.htmlTitle}}</span>
{{else}}
<span class="title">{{label.title}}</span>
{{/if}}

View File

@ -22,19 +22,23 @@
{{/if}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.editable}} {{i18n "admin.user_fields.editable.title"}}
{{input type="checkbox" checked=buffered.editable}} <span>{{i18n "admin.user_fields.editable.title"}}</span>
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.required}} {{i18n "admin.user_fields.required.title"}}
{{input type="checkbox" checked=buffered.required}} <span>{{i18n "admin.user_fields.required.title"}}</span>
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.show_on_profile}} {{i18n "admin.user_fields.show_on_profile.title"}}
{{input type="checkbox" checked=buffered.show_on_profile}} <span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n "admin.user_fields.show_on_user_card.title"}}
{{input type="checkbox" checked=buffered.show_on_user_card}} <span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
{{/admin-form-row}}
{{#admin-form-row wrapLabel="true"}}
{{input type="checkbox" checked=buffered.searchable}} <span>{{i18n "admin.user_fields.searchable.title"}}</span>
{{/admin-form-row}}
{{#admin-form-row}}

View File

@ -1,13 +1,15 @@
{{#admin-nav}}
{{nav-item route="adminCustomizeThemes" label="admin.customize.theme.title" class="admin-customize-themes"}}
{{nav-item route="adminCustomize.colors" label="admin.customize.colors.title" class="admin-customize-colors"}}
{{nav-item route="adminSiteText" label="admin.site_text.title" class="admin-customize-site-text"}}
{{nav-item route="adminCustomizeEmailTemplates" label="admin.customize.email_templates.title" class="admin-customize-email-templates"}}
{{nav-item route="adminCustomizeEmailStyle" label="admin.customize.email_style.title" class="admin-customize-email-styles"}}
{{nav-item route="adminUserFields" label="admin.user_fields.title" class="admin-customize-user-fields"}}
{{nav-item route="adminEmojis" label="admin.emoji.title" class="admin-customize-emojis"}}
{{nav-item route="adminPermalinks" label="admin.permalink.title" class="admin-customize-permalinks"}}
{{nav-item route="adminEmbedding" label="admin.embedding.title" class="admin-customize-embedding"}}
{{#if currentUser.admin}}
{{nav-item route="adminCustomizeThemes" label="admin.customize.theme.title" class="admin-customize-themes"}}
{{nav-item route="adminCustomize.colors" label="admin.customize.colors.title" class="admin-customize-colors"}}
{{nav-item route="adminSiteText" label="admin.site_text.title" class="admin-customize-site-text"}}
{{nav-item route="adminCustomizeEmailTemplates" label="admin.customize.email_templates.title" class="admin-customize-email-templates"}}
{{nav-item route="adminCustomizeEmailStyle" label="admin.customize.email_style.title" class="admin-customize-email-styles"}}
{{nav-item route="adminUserFields" label="admin.user_fields.title" class="admin-customize-user-fields"}}
{{nav-item route="adminEmojis" label="admin.emoji.title" class="admin-customize-emojis"}}
{{nav-item route="adminPermalinks" label="admin.permalink.title" class="admin-customize-permalinks"}}
{{nav-item route="adminEmbedding" label="admin.embedding.title" class="admin-customize-embedding"}}
{{/if}}
{{nav-item route="adminWatchedWords" label="admin.watched_words.title" class="admin-customize-watched-words"}}
{{/admin-nav}}

View File

@ -80,6 +80,10 @@
<div class="label">{{i18n "admin.customize.theme.public_key"}}</div>
{{textarea readonly=true value=publicKey}}
</div>
{{else}}
{{#if privateChecked}}
<div class="public-key-note">{{i18n "admin.customize.theme.public_key_note"}}</div>
{{/if}}
{{/if}}
{{/if}}
</div>

View File

@ -1,12 +1,14 @@
import { cancel, later, run, schedule, throttle } from "@ember/runloop";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed, {
bind,
observes,
} from "discourse-common/utils/decorators";
import Component from "@ember/component";
import Composer from "discourse/models/composer";
import KeyEnterEscape from "discourse/mixins/key-enter-escape";
import afterTransition from "discourse/lib/after-transition";
import discourseDebounce from "discourse-common/lib/debounce";
import { headerHeight } from "discourse/components/site-header";
import { iOSWithVisualViewport } from "discourse/lib/utilities";
import positioningWorkaround from "discourse/lib/safari-hacks";
const START_EVENTS = "touchstart mousedown";
@ -146,12 +148,13 @@ export default Component.extend(KeyEnterEscape, {
this.appEvents.trigger("composer:resize-started");
});
if (iOSWithVisualViewport()) {
if (this._visualViewportResizing()) {
this.viewportResize();
window.visualViewport.addEventListener("resize", this.viewportResize);
}
},
@bind
viewportResize() {
const composerVH = window.visualViewport.height * 0.01,
doc = document.documentElement;
@ -159,26 +162,34 @@ export default Component.extend(KeyEnterEscape, {
doc.style.setProperty("--composer-vh", `${composerVH}px`);
const viewportWindowDiff =
window.innerHeight - window.visualViewport.height;
this.windowInnerHeight - window.visualViewport.height;
viewportWindowDiff
viewportWindowDiff > 0
? doc.classList.add("keyboard-visible")
: doc.classList.remove("keyboard-visible");
// adds bottom padding when using a hardware keyboard and the accessory bar is visible
// accessory bar height is 55px, using 75 allows a small buffer
doc.style.setProperty(
"--composer-ipad-padding",
`${viewportWindowDiff < 75 ? viewportWindowDiff : 0}px`
);
},
if (viewportWindowDiff < 75) {
doc.style.setProperty(
"--composer-ipad-padding",
`${viewportWindowDiff}px`
);
} else {
doc.style.setProperty("--composer-ipad-padding", "0px");
}
_visualViewportResizing() {
return (
(this.capabilities.isIpadOS || this.site.mobileView) &&
window.visualViewport !== undefined
);
},
didInsertElement() {
this._super(...arguments);
if (this._visualViewportResizing()) {
this.set("windowInnerHeight", window.innerHeight);
}
this.setupComposerResizeEvents();
const resize = () => run(() => this.resize());
@ -199,7 +210,7 @@ export default Component.extend(KeyEnterEscape, {
willDestroyElement() {
this._super(...arguments);
this.appEvents.off("composer:resize", this, this.resize);
if (iOSWithVisualViewport()) {
if (this._visualViewportResizing()) {
window.visualViewport.removeEventListener("resize", this.viewportResize);
}
},

View File

@ -540,6 +540,10 @@ export default Component.extend({
schedule("afterRender", () => {
let found = this.warnedGroupMentions || [];
$preview.find(".mention-group.notify").each((idx, e) => {
if (this._isInQuote(e)) {
return;
}
const $e = $(e);
let name = $e.data("name");
if (found.indexOf(name) === -1) {
@ -860,6 +864,30 @@ export default Component.extend({
this.send("togglePreview");
},
_isInQuote(element) {
let parent = element.parentElement;
while (parent && !this._isPreviewRoot(parent)) {
if (this._isQuote(parent)) {
return true;
}
parent = parent.parentElement;
}
return false;
},
_isPreviewRoot(element) {
return (
element.tagName === "DIV" &&
element.classList.contains("d-editor-preview")
);
},
_isQuote(element) {
return element.tagName === "ASIDE" && element.classList.contains("quote");
},
actions: {
importQuote(toolbarEvent) {
this.importQuote(toolbarEvent);

View File

@ -20,6 +20,7 @@ export default Component.extend({
ariaControls: null,
translatedAriaLabel: null,
forwardEvent: false,
preventFocus: false,
isLoading: computed({
set(key, value) {
@ -133,4 +134,10 @@ export default Component.extend({
return false;
},
mouseDown(event) {
if (this.preventFocus) {
event.preventDefault();
}
},
});

View File

@ -81,22 +81,24 @@ class Toolbar {
];
this.addButton({
trimLeading: true,
id: "bold",
group: "fontStyles",
icon: "bold",
label: getButtonLabel("composer.bold_label", "B"),
shortcut: "B",
preventFocus: true,
trimLeading: true,
perform: (e) => e.applySurround("**", "**", "bold_text"),
});
this.addButton({
trimLeading: true,
id: "italic",
group: "fontStyles",
icon: "italic",
label: getButtonLabel("composer.italic_label", "I"),
shortcut: "I",
preventFocus: true,
trimLeading: true,
perform: (e) => e.applySurround("*", "*", "italic_text"),
});
@ -105,6 +107,7 @@ class Toolbar {
id: "link",
group: "insertions",
shortcut: "K",
preventFocus: true,
trimLeading: true,
sendAction: (event) => this.context.send("showLinkModal", event),
});
@ -115,6 +118,7 @@ class Toolbar {
group: "insertions",
icon: "quote-right",
shortcut: "Shift+9",
preventFocus: true,
perform: (e) =>
e.applyList("> ", "blockquote_text", {
applyEmptyLines: true,
@ -126,6 +130,8 @@ class Toolbar {
id: "code",
group: "insertions",
shortcut: "Shift+C",
preventFocus: true,
trimLeading: true,
action: (...args) => this.context.send("formatCode", args),
});
@ -135,6 +141,7 @@ class Toolbar {
icon: "list-ul",
shortcut: "Shift+8",
title: "composer.ulist_title",
preventFocus: true,
perform: (e) => e.applyList("* ", "list_item"),
});
@ -144,6 +151,7 @@ class Toolbar {
icon: "list-ol",
shortcut: "Shift+7",
title: "composer.olist_title",
preventFocus: true,
perform: (e) =>
e.applyList(
(i) => (!i ? "1. " : `${parseInt(i, 10) + 1}. `),
@ -158,6 +166,7 @@ class Toolbar {
icon: "exchange-alt",
shortcut: "Shift+6",
title: "composer.toggle_direction",
preventFocus: true,
perform: (e) => e.toggleDirection(),
});
}
@ -180,6 +189,7 @@ class Toolbar {
perform: button.perform || function () {},
trimLeading: button.trimLeading,
popupMenu: button.popupMenu || false,
preventFocus: button.preventFocus || false,
};
if (button.sendAction) {
@ -412,6 +422,9 @@ export default Component.extend({
);
loadScript("/javascripts/diffhtml.min.js").then(() => {
// changing the contents of the preview element between two uses of
// diff.innerHTML did not apply the diff correctly
window.diff.release(this.element.querySelector(".d-editor-preview"));
window.diff.innerHTML(
this.element.querySelector(".d-editor-preview"),
cookedElement.innerHTML,
@ -603,7 +616,7 @@ export default Component.extend({
},
_getSelected(trimLeading, opts) {
if (!this.ready) {
if (!this.ready || !this.element) {
return;
}
@ -638,18 +651,20 @@ export default Component.extend({
}
},
_selectText(from, length) {
_selectText(from, length, opts = { scroll: true }) {
next(() => {
const textarea = this.element.querySelector("textarea.d-editor-input");
const $textarea = $(textarea);
const oldScrollPos = $textarea.scrollTop();
if (!this.capabilities.isIOS || safariHacksDisabled()) {
$textarea.focus();
}
textarea.selectionStart = from;
textarea.selectionEnd = from + length;
$textarea.trigger("change");
$textarea.scrollTop(oldScrollPos);
if (opts.scroll) {
const oldScrollPos = $textarea.scrollTop();
if (!this.capabilities.isIOS || safariHacksDisabled()) {
$textarea.focus();
}
$textarea.scrollTop(oldScrollPos);
}
});
},
@ -1046,7 +1061,8 @@ export default Component.extend({
const selected = this._getSelected(button.trimLeading);
const toolbarEvent = {
selected,
selectText: (from, length) => this._selectText(from, length),
selectText: (from, length) =>
this._selectText(from, length, { scroll: false }),
applySurround: (head, tail, exampleKey, opts) =>
this._applySurround(selected, head, tail, exampleKey, opts),
applyList: (head, exampleKey, opts) =>

View File

@ -7,16 +7,26 @@ import { action } from "@ember/object";
import loadScript from "discourse/lib/load-script";
import { schedule } from "@ember/runloop";
function isInputDateSupported() {
const input = document.createElement("input");
const value = "a";
input.setAttribute("type", "date");
input.setAttribute("value", value);
return input.value !== value;
}
export default Component.extend({
classNames: ["d-date-input"],
date: null,
_picker: null,
@discourseComputed("site.mobileView")
inputType(mobileView) {
return mobileView ? "date" : "text";
inputType() {
return this.useNativePicker ? "date" : "text";
},
useNativePicker: isInputDateSupported(),
click(event) {
event.stopPropagation();
},
@ -32,7 +42,7 @@ export default Component.extend({
let promise;
const container = document.getElementById(this.containerId);
if (this.site.mobileView) {
if (this.useNativePicker) {
promise = this._loadNativePicker(container);
} else {
promise = this._loadPikadayPicker(container);

View File

@ -35,9 +35,13 @@ export default Component.extend({
},
removeFromGroup() {
this.model
const model = this.model;
model
.removeMember(this.currentUser)
.then(() => this.model.set("is_group_user", false))
.then(() => {
model.set("is_group_user", false);
this.appEvents.trigger("group:leave", model);
})
.catch(popupAjaxError)
.finally(() => this.set("updatingMembership", false));
},
@ -52,6 +56,7 @@ export default Component.extend({
.addMembers(this.currentUser.get("username"))
.then(() => {
model.set("is_group_user", true);
this.appEvents.trigger("group:join", model);
})
.catch(popupAjaxError)
.finally(() => {

View File

@ -1,10 +1,16 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { prioritizeNameInUx } from "discourse/lib/settings";
import { propertyEqual } from "discourse/lib/computed";
export default Component.extend({
classNameBindings: [":user-stream-item", ":item", "moderatorAction"],
classNameBindings: [
":user-stream-item",
":item",
"moderatorAction",
"primaryGroup",
],
@discourseComputed("post.url")
postUrl(url) {
@ -14,4 +20,19 @@ export default Component.extend({
"post.post_type",
"site.post_types.moderator_action"
),
@discourseComputed("post.user")
name() {
if (prioritizeNameInUx(this.post.user.name)) {
return this.post.user.name;
}
return this.post.user.username;
},
@discourseComputed("post.user")
primaryGroup() {
if (this.post.user.primary_group_name) {
return `group-${this.post.user.primary_group_name}`;
}
},
});

View File

@ -23,14 +23,12 @@ export default Component.extend(bufferedProperty("model"), {
},
@discourseComputed(
"buffered.isSaving",
"buffered.name",
"buffered.tag_names",
"buffered.permissions"
)
savingDisabled(isSaving, name, tagNames, permissions) {
cannotSave(name, tagNames, permissions) {
return (
isSaving ||
isEmpty(name) ||
isEmpty(tagNames) ||
(!this.everyoneSelected(permissions) &&
@ -116,6 +114,11 @@ export default Component.extend(bufferedProperty("model"), {
},
save() {
if (this.cannotSave) {
bootbox.alert(I18n.t("tagging.groups.cannot_save"));
return false;
}
const attrs = this.buffered.getProperties(
"name",
"tag_names",

View File

@ -88,7 +88,6 @@ export default Component.extend({
}
this._bindKeyboardShortcuts();
this._loadLastUsedCustomDatetime();
},
@observes("prefilledDatetime")
@ -134,7 +133,7 @@ export default Component.extend({
if (lastTime && lastDate) {
let parsed = parseCustomDatetime(lastDate, lastTime, this.userTimezone);
if (parsed < now(this.userTimezone)) {
if (!parsed.isValid() || parsed < now(this.userTimezone)) {
return;
}
@ -175,6 +174,8 @@ export default Component.extend({
"userTimezone"
)
options(additionalOptionsToShow, hiddenOptions, customOptions, userTimezone) {
this._loadLastUsedCustomDatetime();
let options = defaultShortcutOptions(userTimezone);
if (additionalOptionsToShow.length > 0) {
@ -255,7 +256,7 @@ export default Component.extend({
this.userTimezone
);
if (customDatetime.isValid()) {
if (customDatetime.isValid() && this.customDate) {
dateTime = customDatetime;
localStorage.lastCustomTime = this.customTime;

View File

@ -151,10 +151,7 @@ export default Component.extend({
const $html = $("html");
const offset = window.pageYOffset || $html.scrollTop();
const progressHeight = this.site.mobileView
? 0
: $("#topic-progress").outerHeight();
const maximumOffset = $("#topic-bottom").offset().top + progressHeight;
const maximumOffset = $("#topic-bottom").offset().top;
const windowHeight = $(window).height();
let composerHeight = $("#reply-control").height() || 0;
const isDocked = offset >= maximumOffset - windowHeight + composerHeight;

View File

@ -15,7 +15,10 @@ export default MountWidget.extend({
return;
}
if (this.user.primary_group_flair_url) {
if (
this.user.primary_group_flair_url ||
this.user.primary_group_flair_bg_color
) {
return {
primary_group_flair_url: this.user.primary_group_flair_url,
primary_group_flair_bg_color: this.user.primary_group_flair_bg_color,

View File

@ -20,9 +20,10 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
classNameBindings: [
"visible:show",
"showBadges",
"user.card_background::no-bg",
"user.card_background_upload_url::no-bg",
"isFixed:fixed",
"usernameClass",
"primaryGroup",
],
allowBackgrounds: setting("allow_profile_backgrounds"),
showBadges: setting("enable_badges"),
@ -157,6 +158,11 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
thisElem.style.backgroundImage = bg;
},
@discourseComputed("user.primary_group_name")
primaryGroup(primaryGroup) {
return `group-${primaryGroup}`;
},
_showCallback(username, $target) {
this._positionCard($target);
this.setProperties({ visible: true, loading: true });

View File

@ -11,6 +11,11 @@ export default Controller.extend(ModalFunctionality, {
gravatarBaseUrl: setting("gravatar_base_url"),
gravatarLoginUrl: setting("gravatar_login_url"),
@discourseComputed("selected", "uploading")
submitDisabled(selected, uploading) {
return selected === "logo" || uploading;
},
@discourseComputed(
"siteSettings.selectable_avatars_enabled",
"siteSettings.selectable_avatars"
@ -22,12 +27,20 @@ export default Controller.extend(ModalFunctionality, {
},
@discourseComputed(
"user.use_logo_small_as_avatar",
"user.avatar_template",
"user.system_avatar_template",
"user.gravatar_avatar_template"
)
selected(avatarTemplate, systemAvatarTemplate, gravatarAvatarTemplate) {
if (avatarTemplate === systemAvatarTemplate) {
selected(
useLogo,
avatarTemplate,
systemAvatarTemplate,
gravatarAvatarTemplate
) {
if (useLogo) {
return "logo";
} else if (avatarTemplate === systemAvatarTemplate) {
return "system";
} else if (avatarTemplate === gravatarAvatarTemplate) {
return "gravatar";

View File

@ -690,8 +690,13 @@ export default Controller.extend({
topic.user_last_posted_at
)
) {
const canPostAt = new moment(topic.user_last_posted_at).add(
topic.slow_mode_seconds,
"seconds"
);
const timeLeft = moment().diff(canPostAt, "seconds");
const message = I18n.t("composer.slow_mode.error", {
duration: durationTextFromSeconds(topic.slow_mode_seconds),
timeLeft: durationTextFromSeconds(timeLeft),
});
bootbox.alert(message);
@ -1242,19 +1247,22 @@ export default Controller.extend({
@discourseComputed("model.category", "model.tags", "lastValidatedAt")
tagValidation(category, tags, lastValidatedAt) {
const tagsArray = tags || [];
if (
this.site.can_tag_topics &&
!this.currentUser.staff &&
category &&
category.minimum_required_tags > tagsArray.length
) {
return EmberObject.create({
failed: true,
reason: I18n.t("composer.error.tags_missing", {
count: category.minimum_required_tags,
}),
lastShownAt: lastValidatedAt,
});
if (this.site.can_tag_topics && !this.currentUser.staff && category) {
if (
category.minimum_required_tags > tagsArray.length ||
(category.required_tag_groups &&
category.min_tags_from_required_group > tagsArray.length)
) {
return EmberObject.create({
failed: true,
reason: I18n.t("composer.error.tags_missing", {
count:
category.minimum_required_tags ||
category.min_tags_from_required_group,
}),
lastShownAt: lastValidatedAt,
});
}
}
},

View File

@ -19,8 +19,9 @@ export default Controller.extend(
invite: null,
invites: null,
autogenerated: false,
showAdvanced: false,
limitToEmail: false,
autogenerated: false,
type: "link",
@ -30,8 +31,11 @@ export default Controller.extend(
});
this.setProperties({
autogenerated: false,
invite: null,
invites: null,
showAdvanced: false,
limitToEmail: false,
autogenerated: false,
});
this.setInvite(Invite.create());
@ -41,7 +45,7 @@ export default Controller.extend(
if (this.autogenerated) {
this.invite
.destroy()
.then(() => this.invites.removeObject(this.invite));
.then(() => this.invites && this.invites.removeObject(this.invite));
}
},
@ -53,7 +57,7 @@ export default Controller.extend(
},
setAutogenerated(value) {
if ((this.autogenerated || !this.invite.id) && !value) {
if (this.invites && (this.autogenerated || !this.invite.id) && !value) {
this.invites.unshiftObject(this.invite);
}
@ -168,6 +172,15 @@ export default Controller.extend(
this.save({ sendEmail: false, copy: true });
},
@action
toggleLimitToEmail() {
const limitToEmail = !this.limitToEmail;
this.setProperties({
limitToEmail,
type: limitToEmail ? "email" : "link",
});
},
@action
saveInvite(sendEmail) {
this.appEvents.trigger("modal-body:clearFlash");
@ -181,5 +194,10 @@ export default Controller.extend(
this.set("buffered.email", result[0].email[0]);
});
},
@action
toggleAdvanced() {
this.toggleProperty("showAdvanced");
},
}
);

View File

@ -5,7 +5,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
import Topic from "discourse/models/topic";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import { equal, or } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, {
@ -16,30 +16,51 @@ export default Controller.extend(ModalFunctionality, {
saveDisabled: false,
enabledUntil: null,
showCustomSelect: equal("selectedSlowMode", "custom"),
durationIsSet: or("hours", "minutes", "seconds"),
init() {
this._super(...arguments);
this.set("slowModes", [
{
id: "600",
name: I18n.t("topic.slow_mode_update.durations.10_minutes"),
},
{
id: "900",
name: I18n.t("topic.slow_mode_update.durations.15_minutes"),
},
{
id: "1800",
name: I18n.t("topic.slow_mode_update.durations.30_minutes"),
},
{
id: "2700",
name: I18n.t("topic.slow_mode_update.durations.45_minutes"),
},
{
id: "3600",
name: I18n.t("topic.slow_mode_update.durations.1_hour"),
},
{
id: "7200",
name: I18n.t("topic.slow_mode_update.durations.2_hours"),
},
{
id: "14400",
name: I18n.t("topic.slow_mode_update.durations.4_hours"),
},
{
id: "86400",
name: I18n.t("topic.slow_mode_update.durations.1_day"),
id: "28800",
name: I18n.t("topic.slow_mode_update.durations.8_hours"),
},
{
id: "604800",
name: I18n.t("topic.slow_mode_update.durations.1_week"),
id: "43200",
name: I18n.t("topic.slow_mode_update.durations.12_hours"),
},
{
id: "86400",
name: I18n.t("topic.slow_mode_update.durations.24_hours"),
},
{
id: "custom",
@ -66,9 +87,9 @@ export default Controller.extend(ModalFunctionality, {
}
},
@discourseComputed("hours", "minutes", "seconds")
submitDisabled(hours, minutes, seconds) {
return this.saveDisabled || !(hours || minutes || seconds);
@discourseComputed("saveDisabled", "durationIsSet", "enabledUntil")
submitDisabled(saveDisabled, durationIsSet, enabledUntil) {
return saveDisabled || !durationIsSet || !enabledUntil;
},
_setFromSeconds(seconds) {

View File

@ -1,37 +1,33 @@
import { action } from "@ember/object";
import BufferedContent from "discourse/mixins/buffered-content";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { oneWay } from "@ember/object/computed";
export default Controller.extend(ModalFunctionality, BufferedContent, {
tagId: oneWay("model.id"),
newTag: null,
@discourseComputed("tagId", "model.id")
renameDisabled(inputTagName, currentTagName) {
@discourseComputed("newTag", "model.id")
renameDisabled(newTag, currentTag) {
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
const newTagName = inputTagName
? inputTagName.replace(filterRegexp, "").trim()
: "";
return newTagName.length === 0 || newTagName === currentTagName;
newTag = newTag ? newTag.replace(filterRegexp, "").trim() : "";
return newTag.length === 0 || newTag === currentTag;
},
actions: {
performRename() {
this.model
.update({ id: this.get("tagId") })
.then((result) => {
this.send("closeModal");
@action
performRename() {
this.model
.update({ id: this.newTag })
.then((result) => {
this.send("closeModal");
if (result.responseJson.tag) {
this.transitionToRoute("tag.show", result.responseJson.tag.id);
} else {
this.flash(extractError(result.responseJson.errors[0]), "error");
}
})
.catch((error) => this.flash(extractError(error), "error"));
},
if (result.responseJson.tag) {
this.transitionToRoute("tag.show", result.responseJson.tag.id);
} else {
this.flash(extractError(result.responseJson.errors[0]), "error");
}
})
.catch((error) => this.flash(extractError(error), "error"));
},
});

View File

@ -49,7 +49,7 @@ export default Controller.extend({
@discourseComputed
priorities() {
return ["low", "medium", "high"].map((priority) => {
return ["any", "low", "medium", "high"].map((priority) => {
return {
id: priority,
name: I18n.t(`review.filters.priority.${priority}`),

View File

@ -0,0 +1,116 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { getAbsoluteURL } from "discourse-common/lib/get-url";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
import Sharing from "discourse/lib/sharing";
import showModal from "discourse/lib/show-modal";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export default Controller.extend(
ModalFunctionality,
bufferedProperty("invite"),
{
onShow() {
this.set("showNotifyUsers", false);
},
@discourseComputed("topic.shareUrl")
topicUrl(url) {
return url ? getAbsoluteURL(url) : null;
},
@discourseComputed(
"topic.{isPrivateMessage,invisible,category.read_restricted}"
)
sources(topic) {
const privateContext =
this.siteSettings.login_required ||
(topic && topic.isPrivateMessage) ||
(topic && topic.invisible) ||
topic.category.read_restricted;
return Sharing.activeSources(
this.siteSettings.share_links,
privateContext
);
},
@action
copied() {
return this.appEvents.trigger("modal-body:flash", {
text: I18n.t("topic.share.copied"),
messageClass: "success",
});
},
@action
onChangeUsers(usernames) {
this.set("users", usernames.uniq());
},
@action
share(source) {
this.set("showNotifyUsers", false);
Sharing.shareSource(source, {
title: this.topic.title,
url: this.topicUrl,
});
},
@action
toggleNotifyUsers() {
if (this.showNotifyUsers) {
this.set("showNotifyUsers", false);
} else {
this.setProperties({
showNotifyUsers: true,
users: [],
});
}
},
@action
notifyUsers() {
if (this.users.length === 0) {
return;
}
ajax(`/t/${this.topic.id}/invite-notify`, {
type: "POST",
data: { usernames: this.users },
})
.then(() => {
this.setProperties({ showNotifyUsers: false });
this.appEvents.trigger("modal-body:flash", {
text: I18n.t("topic.share.notify_users.success", {
count: this.users.length,
username: this.users[0],
}),
messageClass: "success",
});
})
.catch((error) => {
this.appEvents.trigger("modal-body:flash", {
text: extractError(error),
messageClass: "error",
});
});
},
@action
inviteUsers() {
this.set("showNotifyUsers", false);
const controller = showModal("create-invite");
controller.set("showAdvanced", true);
controller.buffered.setProperties({
topicId: this.topic.id,
topicTitle: this.topic.title,
});
controller.save({ autogenerated: true });
},
}
);

View File

@ -77,20 +77,32 @@ addBulkButton("showTagTopics", "change_tags", {
icon: "tag",
class: "btn-default",
enabledSetting: "tagging_enabled",
buttonVisible: function () {
return this.currentUser.staff;
},
});
addBulkButton("showAppendTagTopics", "append_tags", {
icon: "tag",
class: "btn-default",
enabledSetting: "tagging_enabled",
buttonVisible: function () {
return this.currentUser.staff;
},
});
addBulkButton("removeTags", "remove_tags", {
icon: "tag",
class: "btn-default",
enabledSetting: "tagging_enabled",
buttonVisible: function () {
return this.currentUser.staff;
},
});
addBulkButton("deleteTopics", "delete", {
icon: "trash-alt",
class: "btn-danger",
buttonVisible: function () {
return this.currentUser.staff;
},
});
// Modal for performing bulk actions on topics
@ -112,7 +124,7 @@ export default Controller.extend(ModalFunctionality, {
if (b.enabledSetting && !this.siteSettings[b.enabledSetting]) {
return false;
}
return b.buttonVisible(topics);
return b.buttonVisible.call(this, topics);
})
);
this.set("modal.modalClass", "topic-bulk-actions-modal small");
@ -121,7 +133,10 @@ export default Controller.extend(ModalFunctionality, {
perform(operation) {
this.set("processedTopicCount", 0);
this.send("changeBulkTemplate", "modal/bulk-progress");
if (this.get("model.topics").length > 20) {
this.send("changeBulkTemplate", "modal/bulk-progress");
}
this.set("loading", true);
return this._processChunks(operation)
@ -215,7 +230,7 @@ export default Controller.extend(ModalFunctionality, {
},
showAppendTagTopics() {
this.set("tags", "");
this.set("tags", null);
this.set("action", "appendTags");
this.set("label", "append_tags");
this.set("title", "choose_append_tags");

View File

@ -154,6 +154,13 @@ export default Controller.extend(CanCheckEmails, {
}
},
@discourseComputed("model.primary_group_name")
primaryGroup(group) {
if (group) {
return `group-${group}`;
}
},
userNotificationLevel: computed(
"currentUser.ignored_ids",
"model.ignored",

View File

@ -0,0 +1,137 @@
import { iconHTML } from "discourse-common/lib/icon-library";
import { prefersReducedMotion } from "discourse/lib/utilities";
import { withPluginApi } from "discourse/lib/plugin-api";
let _gifClickHandlers = {};
function _pauseAnimation(img, opts = {}) {
let canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
canvas.setAttribute("aria-hidden", "true");
canvas.setAttribute("role", "presentation");
if (opts.manualPause) {
img.classList.add("manually-paused");
}
img.parentNode.classList.add("paused-animated-image");
img.parentNode.insertBefore(canvas, img);
}
function _resumeAnimation(img) {
img.previousSibling.remove();
img.parentNode.classList.remove("paused-animated-image", "manually-paused");
}
function animatedImgs() {
return document.querySelectorAll("img.animated:not(.manually-paused)");
}
export default {
name: "animated-images-pause-on-click",
initialize() {
withPluginApi("0.8.7", (api) => {
function _cleanUp() {
Object.values(_gifClickHandlers || {}).forEach((handler) => {
handler.removeEventListener("click", _handleEvent);
handler.removeEventListener("load", _handleEvent);
});
_gifClickHandlers = {};
}
function _handleEvent(event) {
const img = event.target;
if (img && !img.previousSibling) {
_pauseAnimation(img, { manualPause: true });
} else {
_resumeAnimation(img);
}
}
function _attachCommands(post, helper) {
if (!helper) {
return;
}
let images = post.querySelectorAll("img.animated");
images.forEach((img) => {
// skip for edge case of multiple animated images in same block
if (img.parentNode.querySelectorAll("img").length > 1) {
return;
}
if (_gifClickHandlers[img.src]) {
_gifClickHandlers[img.src].removeEventListener(
"click",
_handleEvent
);
_gifClickHandlers[img.src].removeEventListener(
"load",
_handleEvent
);
delete _gifClickHandlers[img.src];
}
_gifClickHandlers[img.src] = img;
img.addEventListener("click", _handleEvent, false);
if (prefersReducedMotion()) {
img.addEventListener("load", _handleEvent, false);
}
img.parentNode.classList.add("pausable-animated-image");
const overlay = document.createElement("div");
overlay.classList.add("animated-image-overlay");
overlay.setAttribute("aria-hidden", "true");
overlay.setAttribute("role", "presentation");
overlay.innerHTML = `${iconHTML("pause")}${iconHTML("play")}`;
img.parentNode.appendChild(overlay);
});
}
api.decorateCookedElement(_attachCommands, {
onlyStream: true,
id: "animated-images-pause-on-click",
});
api.cleanupStream(_cleanUp);
// paused on load when prefers-reduced-motion is active, no need for blur/focus events
if (!prefersReducedMotion()) {
window.addEventListener("blur", this.blurEvent);
window.addEventListener("focus", this.focusEvent);
}
});
},
blurEvent() {
animatedImgs().forEach((img) => {
if (
img.parentNode.querySelectorAll("img").length === 1 &&
!img.previousSibling
) {
_pauseAnimation(img);
}
});
},
focusEvent() {
animatedImgs().forEach((img) => {
if (
img.parentNode.querySelectorAll("img").length === 1 &&
img.previousSibling
) {
_resumeAnimation(img);
}
});
},
teardown() {
window.removeEventListener("blur", this.blurEvent);
window.removeEventListener("focus", this.focusEvent);
},
};

View File

@ -7,8 +7,8 @@ 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)
// 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
@ -88,7 +88,15 @@ export default {
const code = button.nextSibling;
if (code) {
clipboardCopy(code.innerText.trim()).then(() => {
// replace any weird whitespace characters with a proper '\u20' whitespace
const text = code.innerText
.replace(
/[\f\v\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]/g,
" "
)
.trim();
clipboardCopy(text).then(() => {
button.classList.add("copied");
const state = button.innerHTML;
button.innerHTML = I18n.t("copy_codeblock.copied");

View File

@ -16,15 +16,18 @@ export default {
const site = container.lookup("site:main");
if (site.mobileView) {
width = $(window).width() - 20;
width = window.innerWidth - 20;
}
const style = "max-width:" + width + "px;" + "max-height:" + height + "px;";
let styles = `max-width:${width}px; max-height:${height}px;`;
$(
'<style id="image-sizing-hack">#reply-control .d-editor-preview img:not(.thumbnail):not(.ytp-thumbnail-image), .cooked img:not(.thumbnail):not(.ytp-thumbnail-image) {' +
style +
"}</style>"
).appendTo("head");
if (siteSettings.disable_image_size_calculations) {
styles = "max-width: 100%; height: auto;";
}
const styleTag = document.createElement("style");
styleTag.id = "image-sizing-hack";
styleTag.innerHTML = `#reply-control .d-editor-preview img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji), .cooked img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {${styles}}`;
document.head.appendChild(styleTag);
},
};

View File

@ -0,0 +1,57 @@
import { withPluginApi } from "discourse/lib/plugin-api";
let _showMoreClickPostsElements = [];
function decorateGithubOneboxBody(element) {
const containers = element.querySelectorAll(
".onebox.githubcommit .show-more-container, .onebox.githubpullrequest .show-more-container, .onebox.githubissue .show-more-container"
);
if (containers.length) {
_showMoreClickPostsElements.push(element);
element.addEventListener("click", _handleClick, false);
}
}
function _handleClick(event) {
if (!event.target.classList.contains("show-more")) {
return;
}
event.preventDefault();
const showMoreContainer = event.target.parentNode;
const bodyContainer = showMoreContainer.parentNode;
showMoreContainer.classList.add("hidden");
bodyContainer.querySelector(".excerpt.hidden").classList.remove("hidden");
return false;
}
function _cleanUp() {
(_showMoreClickPostsElements || []).forEach((element) => {
element.removeEventListener("click", _handleClick);
});
_showMoreClickPostsElements = [];
}
export default {
name: "onebox-decorators",
initialize() {
withPluginApi("0.8.42", (api) => {
api.decorateCookedElement(
(element) => {
decorateGithubOneboxBody(element);
},
{
id: "onebox-github-body",
}
);
api.cleanupStream(_cleanUp);
});
},
};

View File

@ -1,8 +1,13 @@
import { later } from "@ember/runloop";
import I18n from "I18n";
import highlightSyntax from "discourse/lib/highlight-syntax";
import lightbox from "discourse/lib/lightbox";
import { iconHTML } from "discourse-common/lib/icon-library";
import { setTextDirections } from "discourse/lib/text-direction";
import { setupLazyLoading } from "discourse/lib/lazy-load-images";
import {
nativeLazyLoading,
setupLazyLoading,
} from "discourse/lib/lazy-load-images";
import { withPluginApi } from "discourse/lib/plugin-api";
export default {
@ -33,7 +38,11 @@ export default {
});
}
setupLazyLoading(api);
if (siteSettings.disable_image_size_calculations) {
nativeLazyLoading(api);
} else {
setupLazyLoading(api);
}
api.decorateCooked(
($elem) => {
@ -103,6 +112,31 @@ export default {
},
{ id: "onebox-source-icons" }
);
api.decorateCookedElement(
(element) => {
element
.querySelectorAll(".video-container")
.forEach((videoContainer) => {
const video = videoContainer.getElementsByTagName("video")[0];
video.addEventListener("loadeddata", () => {
later(() => {
if (video.videoWidth === 0 || video.videoHeight === 0) {
const notice = document.createElement("div");
notice.className = "notice";
notice.innerHTML =
iconHTML("exclamation-triangle") +
" " +
I18n.t("cannot_render_video");
videoContainer.appendChild(notice);
}
}, 500);
});
});
},
{ id: "discourse-video-codecs" }
);
});
},
};

View File

@ -4,9 +4,7 @@ export default {
name: "register-service-worker",
initialize(container) {
const isSecured =
document.location.protocol === "https:" ||
location.hostname === "localhost";
const isSecured = document.location.protocol === "https:";
if (isSecured && "serviceWorker" in navigator) {
let { serviceWorkerURL } = container.lookup("session:main");

View File

@ -25,39 +25,10 @@ export default {
},
title: "topic.share.help",
action() {
const panels = [
{
id: "share",
title: "topic.share.extended_title",
model: {
topic: this.topic,
},
},
];
if (this.canInviteTo && !this.inviteDisabled) {
let invitePanelTitle;
if (this.isPM) {
invitePanelTitle = "topic.invite_private.title";
} else if (this.invitingToTopic) {
invitePanelTitle = "topic.invite_reply.title";
} else {
invitePanelTitle = "user.invited.create";
}
panels.push({
id: "invite",
title: invitePanelTitle,
model: {
inviteModel: this.topic,
},
});
}
showModal("share-and-invite", {
modalClass: "share-and-invite",
panels,
const controller = showModal("share-topic");
controller.setProperties({
allowInvites: this.canInviteTo && !this.inviteDisabled,
topic: this.topic,
});
},
dropdown() {

View File

@ -85,6 +85,10 @@ export default function (options) {
return this;
}
if (options && typeof options.preserveKey === "undefined") {
options.preserveKey = true;
}
const disabled = options && options.disabled;
let wrap = null;
let autocompleteOptions = null;
@ -182,7 +186,7 @@ export default function (options) {
});
}
let completeTerm = function (term) {
let completeTerm = async function (term) {
if (term) {
if (isInput) {
me.val("");
@ -192,14 +196,14 @@ export default function (options) {
addInputSelectedItem(term, true);
} else {
if (options.transformComplete) {
term = options.transformComplete(term);
term = await options.transformComplete(term);
}
if (term) {
let text = me.val();
text =
text.substring(0, completeStart) +
(options.key || "") +
(options.preserveKey ? options.key || "" : "") +
term +
" " +
text.substring(completeEnd + 1, text.length);

View File

@ -121,3 +121,20 @@ export function setupLazyLoading(api) {
}
);
}
export function nativeLazyLoading(api) {
api.decorateCookedElement(
(post) =>
forEachImage(post, (img) => {
img.loading = "lazy";
if (img.dataset.smallUpload) {
img.style = `background-image: url(${img.dataset.smallUpload}); background-size: cover;`;
}
}),
{
onlyStream: true,
id: "discourse-lazy-load-after-adopt",
afterAdopt: true,
}
);
}

View File

@ -11,11 +11,18 @@ const AJAX_FAILURE_DELAYS = [5000, 10000, 20000, 40000];
const ALLOWED_AJAX_FAILURES = [405, 429, 500, 501, 502, 503, 504];
export default class {
constructor(topicTrackingState, siteSettings, session, currentUser) {
constructor(
topicTrackingState,
siteSettings,
session,
currentUser,
appEvents
) {
this.topicTrackingState = topicTrackingState;
this.siteSettings = siteSettings;
this.session = session;
this.currentUser = currentUser;
this.appEvents = appEvents;
this.reset();
this._consolidatedTimings = [];
}
@ -139,15 +146,16 @@ export default class {
this._ajaxFailures = this._ajaxFailures || 0;
const { timings, topicTime, topicId } = this._consolidatedTimings.pop();
const data = {
timings: timings,
topic_time: topicTime,
topic_id: topicId,
};
this._inProgress = true;
ajax("/topics/timings", {
data: {
timings: timings,
topic_time: topicTime,
topic_id: topicId,
},
data,
cache: false,
type: "POST",
headers: {
@ -162,6 +170,7 @@ export default class {
const postNumbers = Object.keys(timings).map((v) => parseInt(v, 10));
topicController.readPosts(topicId, postNumbers);
}
this.appEvents.trigger("topic:timings-sent", data);
})
.catch((e) => {
if (ALLOWED_AJAX_FAILURES.indexOf(e.jqXHR.status) > -1) {

View File

@ -10,7 +10,7 @@ import offsetCalculator from "discourse/lib/offset-calculator";
import { setOwner } from "@ember/application";
const rewrites = [];
const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
export const TOPIC_URL_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
// We can add links here that have server side responses but not client side.
const SERVER_SIDE_ONLY = [
@ -348,11 +348,11 @@ const DiscourseURL = EmberObject.extend({
same topic, use replaceState and instruct our controller to load more posts.
**/
navigatedToPost(oldPath, path, routeOpts) {
const newMatches = TOPIC_REGEXP.exec(path);
const newMatches = TOPIC_URL_REGEXP.exec(path);
const newTopicId = newMatches ? newMatches[2] : null;
if (newTopicId) {
const oldMatches = TOPIC_REGEXP.exec(oldPath);
const oldMatches = TOPIC_URL_REGEXP.exec(oldPath);
const oldTopicId = oldMatches ? oldMatches[2] : null;
// If the topic_id is the same

View File

@ -433,6 +433,10 @@ export function isiOSPWA() {
return window.matchMedia("(display-mode: standalone)").matches && caps.isIOS;
}
export function prefersReducedMotion() {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
export function isAppWebview() {
return window.ReactNativeWebView !== undefined;
}

View File

@ -1,6 +1,6 @@
import Mixin from "@ember/object/mixin";
import discourseDebounce from "discourse-common/lib/debounce";
import { later } from "@ember/runloop";
import { cancel, later } from "@ember/runloop";
const helper = {
offset() {
@ -12,11 +12,13 @@ const helper = {
export default Mixin.create({
queueDockCheck: null,
_initialTimer: null,
_queuedTimer: null,
init() {
this._super(...arguments);
this.queueDockCheck = () => {
discourseDebounce(this, this.safeDockCheck, 5);
this._queuedTimer = discourseDebounce(this, this.safeDockCheck, 5);
};
},
@ -34,11 +36,17 @@ export default Mixin.create({
$(document).bind("touchmove.discourse-dock", this.queueDockCheck);
// dockCheck might happen too early on full page refresh
later(this, this.safeDockCheck, 50);
this._initialTimer = later(this, this.safeDockCheck, 50);
},
willDestroyElement() {
this._super(...arguments);
if (this._queuedTimer) {
cancel(this._queuedTimer);
}
cancel(this._initialTimer);
$(window).unbind("scroll.discourse-dock", this.queueDockCheck);
$(document).unbind("touchmove.discourse-dock", this.queueDockCheck);
},

View File

@ -151,8 +151,23 @@ const Composer = RestModel.extend({
@discourseComputed("category")
minimumRequiredTags(category) {
return category && category.minimum_required_tags > 0
? category.minimum_required_tags
if (category) {
if (category.required_tag_groups) {
return category.min_tags_from_required_group;
} else {
return category.minimum_required_tags > 0
? category.minimum_required_tags
: null;
}
}
return null;
},
@discourseComputed("category")
requiredTagGroups(category) {
return category && category.required_tag_groups
? category.required_tag_groups
: null;
},

View File

@ -614,6 +614,12 @@ Topic.reopenClass({
MUTED: 0,
},
munge(json) {
// ensure we are not overriding category computed property
delete json.category;
return json;
},
createActionSummary(result) {
if (result.actions_summary) {
const lookup = EmberObject.create();

View File

@ -74,7 +74,8 @@ export default {
topicTrackingState,
siteSettings,
session,
currentUser
currentUser,
app.appEvents
);
app.register("service:screen-track", screenTrack, { instantiate: false });

View File

@ -95,7 +95,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
}
return findTopicList(this.store, this.topicTrackingState, filter, params, {
cached: true,
cached: this.isPoppedState(transition),
}).then((list) => {
if (list.topic_list.tags && list.topic_list.tags.length === 1) {
// Update name of tag (case might be different)

View File

@ -3,8 +3,9 @@ import DiscourseURL from "discourse/lib/url";
import { ajax } from "discourse/lib/ajax";
export default DiscourseRoute.extend({
model(params, transition) {
const path = params.path;
model(_, transition) {
const path = transition.intent.url;
return ajax("/permalink-check.json", {
data: { path },
}).then((results) => {

View File

@ -10,6 +10,7 @@
class="logo"
width=c.uploaded_logo.width
height=c.uploaded_logo.height
alt=""
}}
{{/if}}
{{/unless}}
@ -34,6 +35,7 @@
{{/if}}
</div>
{{/unless}}
{{plugin-outlet name="category-box-below-each-category" connectorTagName="" tagName="" args=(hash category=c)}}
</div>
</div>
{{/each}}

View File

@ -9,6 +9,7 @@
class="logo"
width=c.uploaded_logo.width
height=c.uploaded_logo.height
alt=""
}}
{{/if}}
</div>
@ -59,7 +60,8 @@
src=sc.uploaded_logo.url
class="logo"
width=sc.uploaded_logo.width
height=sc.uploaded_logo.height}}
height=sc.uploaded_logo.height
alt=""}}
</span>
{{category-link sc hideParent="true"}}
</a>
@ -68,6 +70,7 @@
{{/if}}
{{/unless}}
</div>
{{plugin-outlet name="category-box-below-each-category" connectorTagName="" tagName="" args=(hash category=c)}}
</div>
</div>
{{/each}}

View File

@ -11,6 +11,7 @@
src=category.uploaded_logo.url
class="category-logo"
width=category.uploaded_logo.width
height=category.uploaded_logo.height}}
height=category.uploaded_logo.height
alt=""}}
{{/if}}
</a>

View File

@ -1,5 +1,5 @@
{{#if src}}
<div class="{{class}} aspect-image" style={{style}}>
<img src={{cdnSrc}} width={{width}} height={{height}}>
<img src={{cdnSrc}} width={{width}} height={{height}} alt={{alt}}>
</div>
{{/if}}

View File

@ -7,7 +7,9 @@
icon="bars"
action=toggleToolbar
title=toggleToolbarTitle
ariaLabel=toggleToolbarTitle}}
ariaLabel=toggleToolbarTitle
preventFocus=true
}}
{{/if}}
{{d-button
@ -15,7 +17,8 @@
icon=toggleIcon
action=toggleComposer
title=toggleTitle
ariaLabel=toggleTitle}}
ariaLabel=toggleTitle
}}
{{#unless site.mobileView}}
{{d-button
@ -23,6 +26,7 @@
icon=fullscreenIcon
action=toggleFullscreen
title=fullscreenTitle
ariaLabel=fullscreenTitle}}
ariaLabel=fullscreenTitle
}}
{{/unless}}
</div>

View File

@ -22,7 +22,9 @@
translatedTitle=b.title
label=b.label
icon=b.icon
class=b.className}}
class=b.className
preventFocus=b.preventFocus
}}
{{/if}}
{{/each}}

View File

@ -6,4 +6,6 @@
input=(action "onChangeDate")
}}
<div class="picker-container"></div>
{{#unless useGlobalPickerContainer}}
<div class="picker-container"></div>
{{/unless}}

View File

@ -3,6 +3,7 @@
date=date
relativeDate=relativeDate
onChange=(action "onChangeDate")
useGlobalPickerContainer=useGlobalPickerContainer
}}
{{/unless}}
@ -19,6 +20,7 @@
date=date
relativeDate=relativeDate
onChange=(action "onChangeDate")
useGlobalPickerContainer=useGlobalPickerContainer
}}
{{/if}}

View File

@ -8,7 +8,7 @@
<div class="section-group">
{{replace-emoji ":grinning:" (hash lazy=true)}}
{{replace-emoji ":smiley:" (hash lazy=true)}}
{{replace-emoji ":smile:" (hash lazy=true)}}
{{replace-emoji ":grinning_face_with_smiling_eyes:" (hash lazy=true)}}
{{replace-emoji ":grin:" (hash lazy=true)}}
{{replace-emoji ":laughing:" (hash lazy=true)}}
{{replace-emoji ":sweat_smile:" (hash lazy=true)}}
@ -78,6 +78,7 @@
{{replace-emoji ":flushed:" (hash lazy=true)}}
{{replace-emoji ":pleading_face:" (hash lazy=true)}}
{{replace-emoji ":frowning:" (hash lazy=true)}}
{{replace-emoji ":frowning_face_with_open_mouth:" (hash lazy=true)}}
{{replace-emoji ":anguished:" (hash lazy=true)}}
{{replace-emoji ":fearful:" (hash lazy=true)}}
{{replace-emoji ":cold_sweat:" (hash lazy=true)}}

View File

@ -12,10 +12,11 @@
</span>
</div>
<div class="group-post-category">{{category-link post.category}}</div>
{{#if post.user.name}}
<div class="group-member-info">
<span class="name">{{post.user.name}}</span>
{{#if post.user.title}}<span class="title">, {{post.user.title}}</span>{{/if}}
{{#if post.user}}
<div class="group-member-info names">
<span class="name">{{name}}</span>
{{#if post.user.title}}<span class="user-title">{{post.user.title}}</span>{{/if}}
{{plugin-outlet name="group-post-additional-member-info" noTags=true args=(hash user=post.user)}}
</div>
{{/if}}
</div>

View File

@ -1,12 +1,6 @@
<div class="flagged-post-header">
{{reviewable-topic-link reviewable=reviewable tagName=""}}
{{#if hasEdits}}
<a href {{action "showEditHistory"}}
class="has-edits {{historyClass}}"
title={{i18n "post.last_edited_on" dateTime=editedDate}}>
{{d-icon "pencil-alt"}}
</a>
{{/if}}
{{reviewable-post-edits reviewable=reviewable tagName=""}}
</div>
<div class="post-contents-wrapper">

View File

@ -0,0 +1,7 @@
{{#if hasEdits}}
<a href {{action "showEditHistory"}}
class="has-edits {{historyClass}}"
title={{i18n "post.last_edited_on" dateTime=editedDate}}>
{{d-icon "pencil-alt"}}
</a>
{{/if}}

View File

@ -0,0 +1,19 @@
<div class="flagged-post-header">
{{reviewable-topic-link reviewable=reviewable tagName=""}}
{{reviewable-post-edits reviewable=reviewable tagName=""}}
</div>
<div class="post-contents-wrapper">
{{reviewable-created-by user=reviewable.target_created_by tagName=""}}
<div class="post-contents">
{{reviewable-post-header reviewable=reviewable createdBy=reviewable.target_created_by tagName=""}}
<div class="post-body">
{{#if reviewable.blank_post}}
<p>{{i18n "review.deleted_post"}}</p>
{{else}}
{{html-safe reviewable.cooked}}
{{/if}}
</div>
{{yield}}
</div>
</div>

View File

@ -1,10 +1,10 @@
<div class="signup-cta alert alert-info">
{{#if session.hideSignupCta}}
<p>
<h3>
{{i18n "signup_cta.hidden_for_session"}}
</p>
</h3>
{{else}}
<p>{{replace-emoji (i18n "signup_cta.intro")}}</p>
<h3>{{replace-emoji (i18n "signup_cta.intro")}}</h3>
<p>{{replace-emoji (i18n "signup_cta.value_prop")}}</p>
<div class="buttons">
@ -13,5 +13,4 @@
<a href {{action "neverShow"}}>{{i18n "signup_cta.hide_forever"}}</a>
</div>
{{/if}}
<div class="clearfix"></div>
</div>

View File

@ -93,7 +93,7 @@
{{d-button
class="btn-default"
action=(action "save")
disabled=savingDisabled
disabled=buffered.isSaving
label="tagging.groups.save"}}
{{d-button

View File

@ -4,23 +4,23 @@
{{clockIcon}} {{notice}}
</span>
<div class="topic-timer-modify">
{{#if showTrashCan}}
{{d-button
ariaLabel="post.controls.remove_timer"
title="post.controls.remove_timer"
icon="trash-alt"
class="btn topic-timer-remove no-text"
action=removeTopicTimer
}}
{{/if}}
{{#if showEdit}}
{{d-button
{{~d-button
ariaLabel="post.controls.edit_timer"
title="post.controls.edit_timer"
icon="pencil-alt"
class="btn topic-timer-edit no-text"
action=showTopicTimerModal
}}
~}}
{{/if}}
{{#if showTrashCan}}
{{~d-button
ariaLabel="post.controls.remove_timer"
title="post.controls.remove_timer"
icon="trash-alt"
class="btn topic-timer-remove no-text"
action=removeTopicTimer
~}}
{{/if}}
</div>
</h3>

View File

@ -94,6 +94,7 @@
options=(hash
categoryId=model.categoryId
minimum=model.minimumRequiredTags
requiredTagGroups=model.requiredTagGroups
)
}}
{{popup-input-tip validation=tagValidation}}

View File

@ -33,15 +33,6 @@
showLogin=(route-action "showLogin")
}}
{{#if displayGroupMessageButton}}
{{d-button
action=(action "messageGroup")
class="btn-primary group-message-button"
icon="envelope"
label="groups.message"
}}
{{/if}}
{{#if currentUser.admin}}
{{#if model.automatic}}
{{d-button
@ -60,6 +51,15 @@
}}
{{/if}}
{{/if}}
{{#if displayGroupMessageButton}}
{{d-button
action=(action "messageGroup")
class="btn-primary group-message-button"
icon="envelope"
label="groups.message"
}}
{{/if}}
</div>
{{plugin-outlet name="group-details-after" args=(hash model=model)}}

View File

@ -102,3 +102,5 @@
{{/conditional-loading-spinner}}
{{/d-section}}
{{plugin-outlet name="after-groups-index-container" tagName=""}}

View File

@ -8,6 +8,12 @@
{{/each}}
</div>
{{else}}
{{#if user.use_logo_small_as_avatar}}
<div class="avatar-choice">
{{radio-button id="logo-small" name="logo" value="logo" selection=selected}}
<label class="radio" for="logo-small">{{bound-avatar-template siteSettings.site_logo_small_url "large"}} {{html-safe (i18n "user.change_avatar.logo_small")}}</label>
</div>
{{/if}}
<div class="avatar-choice">
{{radio-button id="system-avatar" name="avatar" value="system" selection=selected}}
<label class="radio" for="system-avatar">{{bound-avatar-template user.system_avatar_template "large"}} {{html-safe (i18n "user.change_avatar.letter_based")}}</label>
@ -28,7 +34,7 @@
{{/if}}
</div>
<div class="avatar-choice">
{{radio-button id="uploaded-avatar" name="avatar" value="uploaded" selection=selected}}
{{radio-button id="uploaded-avatar" name="avatar" value="custom" selection=selected}}
<label class="radio" for="uploaded-avatar">
{{#if user.custom_avatar_template}}
{{bound-avatar-template user.custom_avatar_template "large"}}
@ -50,7 +56,7 @@
{{#unless siteSettings.selectable_avatars_enabled}}
<div class="modal-footer">
{{d-button action=(action "saveAvatarSelection") class="btn-primary" disabled=uploading label="save"}}
{{d-button action=(action "saveAvatarSelection") class="btn-primary" disabled=submitDisabled label="save"}}
{{d-modal-cancel close=(route-action "closeModal")}}
</div>
{{/unless}}

View File

@ -1,26 +1,40 @@
{{#d-modal-body title=(if invite.id "user.invited.invite.edit_title" "user.invited.invite.new_title")}}
<form>
<div class="input-group invite-link-field">
<label for="invite_link">{{i18n "user.invited.invite.instructions"}}</label>
{{input name="invite_link"
class="invite-link"
value=invite.link
readonly=true}}
{{copy-button selector="input.invite-link" copied=(action "copied")}}
<div class="input-group invite-link">
<label for="invite-link">{{i18n "user.invited.invite.instructions"}}</label>
<div class="invite-input-with-button">
{{input
name="invite-link"
class="invite-link"
value=invite.link
readonly=true
}}
{{copy-button selector="input.invite-link" copied=(action "copied")}}
</div>
</div>
<p>{{expiresAtLabel}}</p>
<div class="input-group invite-type">
<div class="radio-group">
{{radio-button id="invite-type-link" name="invite-type" value="link" selection=type}}
<label for="invite-type-link">{{i18n "user.invited.invite.type_link"}}</label>
</div>
{{input type="checkbox" id="invite-type" checked=limitToEmail click=(action "toggleLimitToEmail")}}
<label for="invite-type">{{i18n "user.invited.invite.restrict_email"}}</label>
<div class="radio-group">
{{radio-button id="invite-type-email" name="invite-type" value="email" selection=type}}
<label for="invite-type-email">{{i18n "user.invited.invite.type_email"}}</label>
</div>
{{#if isEmail}}
<div class="invite-input-with-button">
{{input
id="invite-email"
value=buffered.email
placeholderKey="topic.invite_reply.email_placeholder"
}}
{{#if capabilities.hasContactPicker}}
{{d-button
icon="address-book"
action=(action "searchContact")
class="btn-primary open-contact-picker"
}}
{{/if}}
</div>
{{/if}}
</div>
{{#if isLink}}
@ -37,33 +51,41 @@
{{/if}}
{{#if isEmail}}
<div class="input-group invite-email-field">
<label for="invite-email">{{i18n "user.invited.invite.email"}}</label>
{{input
id="invite-email"
value=buffered.email
placeholderKey="topic.invite_reply.email_placeholder"
}}
{{#if capabilities.hasContactPicker}}
{{d-button
icon="address-book"
action=(action "searchContact")
class="btn-primary open-contact-picker"
}}
{{#if showAdvanced}}
{{#if currentUser.staff}}
<div class="input-group invite-custom-message">
<label for="invite-message">{{i18n "user.invited.invite.custom_message"}}</label>
{{textarea id="invite-message" value=buffered.custom_message}}
</div>
{{/if}}
</div>
{{/if}}
{{/if}}
{{#if currentUser.staff}}
<p id="invite-show-advanced">
{{#if showAdvanced}}
<a href {{action (mut showAdvanced) false}}>{{d-icon "caret-down"}} {{i18n "user.invited.invite.hide_advanced"}}</a>
{{else}}
<a href {{action (mut showAdvanced) true}}>{{d-icon "caret-right"}} {{i18n "user.invited.invite.show_advanced"}}</a>
{{/if}}
</p>
{{#if showAdvanced}}
{{#if currentUser.staff}}
<div class="input-group invite-to-topic">
{{choose-topic
selectedTopicId=buffered.topicId
topicTitle=buffered.topicTitle
additionalFilters="status:public"
label="user.invited.invite.invite_to_topic"
}}
</div>
{{else}}
<div class="input-group">
<label for="invite-topic">{{i18n "user.invited.invite.invite_to_topic"}}</label>
{{input
name="invite-topic"
class="invite-topic"
value=buffered.topicTitle
readonly=true
}}
</div>
{{/if}}
{{/if}}
{{#if showAdvanced}}
{{#if showAdvanced}}
{{#if currentUser.staff}}
<div class="input-group invite-to-groups">
<label>{{i18n "user.invited.invite.add_to_groups"}}</label>
{{group-chooser
@ -73,16 +95,11 @@
onChange=(action (mut buffered.groupIds))
}}
</div>
{{/if}}
{{/if}}
<div class="input-group invite-to-topic">
{{choose-topic
selectedTopicId=buffered.topicId
topicTitle=buffered.topicTitle
additionalFilters="status:public"
label="user.invited.invite.invite_to_topic"
}}
</div>
{{#if showAdvanced}}
{{#if currentUser.staff}}
<div class="input-group invite-expires-at">
{{future-date-input
displayLabel=(i18n "user.invited.invite.expires_at")
@ -92,13 +109,6 @@
onChangeInput=(action (mut buffered.expires_at))
}}
</div>
{{#if isEmail}}
<div class="input-group invite-custom-message">
<label for="invite-message">{{i18n "user.invited.invite.custom_message"}}</label>
{{textarea id="invite-message" value=buffered.custom_message}}
</div>
{{/if}}
{{/if}}
{{/if}}
</form>
@ -123,9 +133,12 @@
}}
{{/if}}
{{d-button
label="cancel"
class="btn-flat"
action=(route-action "closeModal")
}}
{{#if currentUser.staff}}
{{d-button
action=(action "toggleAdvanced")
class="show-advanced"
icon="cog"
title=(if showAdvanced "user.invited.invite.hide_advanced" "user.invited.invite.show_advanced")
}}
{{/if}}
</div>

View File

@ -1,5 +1,5 @@
<form {{action "ok" on="submit"}}>
{{#d-modal-body title="composer.link_dialog_title" class="insert-link"}}
{{#d-modal-body title="composer.link_dialog_title" class="insert-link"}}
<form id="insert-hyperlink-form" {{action "ok" on="submit"}}>
<div class="inputs">
{{text-field
value=linkUrl
@ -18,7 +18,8 @@
class="search-link"
href={{result.url}}
onclick={{action "linkClick"}}
data-title={{result.fancy_title}}>
data-title={{result.fancy_title}}
>
{{topic-status topic=result disableActions=true}}
{{replace-emoji result.fancy_title}}
<div class="search-category">
@ -36,10 +37,21 @@
<div class="inputs">
{{text-field value=linkText placeholderKey="composer.link_optional_text" class="link-text"}}
</div>
{{/d-modal-body}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary" label="composer.modal_ok" action=(action "ok") type="submit"}}
{{d-button class="btn-danger" label="composer.modal_cancel" action=(action "cancel")}}
</div>
</form>
<div class="modal-footer">
{{d-button
class="btn-primary"
label="composer.modal_ok"
action=(action "ok")
type="submit"
form="insert-hyperlink-form"
}}
{{d-button
class="btn-danger"
label="composer.modal_cancel"
action=(action "cancel")
}}
</div>

View File

@ -3,13 +3,19 @@
{{i18n "tagging.rename_instructions"}}
</label>
<div class="controls">
{{input value=tagId maxlength=siteSettings.max_tag_length}}
{{input
value=(readonly model.id)
maxlength=siteSettings.max_tag_length
input=(action (mut newTag) value="target.value")
}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary"
action=(action "performRename")
label="tagging.rename_tag"
disabled=renameDisabled}}
{{d-button
class="btn-primary"
action=(action "performRename")
label="tagging.rename_tag"
disabled=renameDisabled
}}
</div>

View File

@ -0,0 +1,61 @@
{{#d-modal-body title="topic.share.title"}}
<form>
<div class="input-group invite-link">
<label for="invite-link">{{i18n "topic.share.instructions"}}</label>
<div class="invite-input-with-button">
{{input
name="invite-link"
class="invite-link"
value=topicUrl
readonly=true
}}
{{copy-button selector="input.invite-link" copied=(action "copied")}}
</div>
</div>
<div class="sources">
{{#each sources as |s|}}
{{share-source source=s title=topic.title action=(action "share")}}
{{/each}}
{{d-button
class="btn-primary"
label="topic.share.notify_users.title"
icon="hand-point-right"
action=(action "toggleNotifyUsers")
}}
{{#if allowInvites}}
{{d-button
class="btn-primary"
label="topic.share.invite_users"
icon="user-plus"
action=(action "inviteUsers")
}}
{{/if}}
</div>
{{#if showNotifyUsers}}
<div class="input-group invite-users">
<label for="invite-users">{{i18n "topic.share.notify_users.instructions"}}</label>
<div class="invite-input-with-button">
{{user-chooser
value=users
onChange=(action "onChangeUsers")
options=(hash
topicId=topic.id
maximum=(unless currentUser.staff 1)
excludeCurrentUser=true
)
}}
{{d-button
icon="check"
class="btn-primary"
disabled=(not users)
action=(action "notifyUsers")
}}
</div>
</div>
{{/if}}
</form>
{{/d-modal-body}}

View File

@ -1,22 +1,24 @@
{{add-category-tag-classes category=category tagName=""}}
{{add-category-tag-classes category=category tagName=""}}
<section class="category-heading">
{{#if category.uploaded_logo.url}}
{{cdn-img
src=category.uploaded_logo.url
class="category-logo"
width=category.uploaded_logo.width
height=category.uploaded_logo.height}}
{{#if category.description}}
<p>{{dir-span category.description}}</p>
{{/if}}
<section class="category-heading">
{{#if category.uploaded_logo.url}}
{{cdn-img
src=category.uploaded_logo.url
class="category-logo"
width=category.uploaded_logo.width
height=category.uploaded_logo.height
alt=""
}}
{{#if category.description}}
<p>{{dir-span category.description}}</p>
{{/if}}
{{/if}}
{{plugin-outlet name="category-heading" args=(hash category=category)}}
</section>
{{plugin-outlet name="category-heading" args=(hash category=category)}}
</section>
{{#d-section class="navigation-container category-navigation"}}
{{d-navigation
category=category
filterMode=filterMode
@ -25,8 +27,12 @@
createTopic=(route-action "createTopic")
createTopicDisabled=cannotCreateTopicOnCategory
hasDraft=currentUser.has_topic_draft
editCategory=(route-action "editCategory" category)}}
{{plugin-outlet name="category-navigation" args=(hash category=category)}}
editCategory=(route-action "editCategory" category)
}}
{{plugin-outlet
name="category-navigation"
args=(hash category=category)
tagName=""
}}
{{/d-section}}

View File

@ -21,6 +21,12 @@
changeTagNotificationLevel=(action "changeTagNotificationLevel")
toggleInfo=(action "toggleInfo")
}}
{{plugin-outlet
name="tag-navigation"
args=(hash category=category tag=tag)
tagName=""
}}
</section>
</div>
</div>

View File

@ -293,7 +293,7 @@
</div>
{{/if}}
{{slow-mode-info topic=model user=currentUser}}
{{slow-mode-info topic=model user=currentUser tagName=""}}
{{topic-timer-info
topicClosed=model.closed

View File

@ -90,10 +90,6 @@
<thead>
<tr>
<th>{{i18n "user.invited.invited_via"}}</th>
{{#if currentUser.staff}}
<th>{{i18n "user.invited.groups"}}</th>
<th>{{i18n "user.invited.topic"}}</th>
{{/if}}
<th>{{i18n "user.invited.sent"}}</th>
<th>{{i18n "user.invited.expires_at"}}</th>
<th></th>
@ -105,33 +101,25 @@
<td class="invite-type">
<div class="label">{{i18n "user.invited.invited_via"}}</div>
{{#if invite.email}}
{{invite.email}}
{{d-icon "envelope"}} {{invite.email}}
{{else}}
{{i18n "user.invited.invited_via_link" key=invite.shortKey count=invite.redemption_count max=invite.max_redemptions_allowed}}
{{d-icon "link"}} {{i18n "user.invited.invited_via_link" key=invite.shortKey count=invite.redemption_count max=invite.max_redemptions_allowed}}
{{/if}}
{{#each invite.groups as |g|}}
<p class="invite-extra"><a href="/g/{{g.name}}">{{d-icon "users"}} {{g.name}}</a></p>
{{/each}}
{{#if invite.topic}}
<p class="invite-extra"><a href={{invite.topic.url}}>{{d-icon "file"}} {{invite.topic.title}}</a></p>
{{/if}}
</td>
{{#if currentUser.staff}}
<td class="invite-groups">
<div class="label">{{i18n "user.invited.groups"}}</div>
{{#each invite.groups as |g|}}
<a href="/g/{{g.name}}">{{g.name}}</a>
{{else}}
&mdash;
{{/each}}
</td>
<td class="invite-topic">
<div class="label">{{i18n "user.invited.topic"}}</div>
{{#if invite.topic}}
<a href={{invite.topic.url}}>{{invite.topic.title}}</a>
{{else}}
&mdash;
{{/if}}
</td>
{{/if}}
<td class="invite-updated-at">
<div class="label">{{i18n "user.invited.sent"}}</div>
{{format-date invite.updated_at}}
</td>
<td class="invite-expires-at">
<div class="label">{{i18n "user.invited.expires_at"}}</div>
{{#if inviteExpired}}
@ -142,6 +130,7 @@
{{raw-date invite.expires_at}}
{{/if}}
</td>
<td class="invite-actions">
{{d-button icon="pencil-alt" action=(action "editInvite" invite) title="user.invited.edit"}}
{{d-button icon="trash-alt" class="cancel" action=(action "destroyInvite" invite) title=(if invite.destroyed "user.invited.removed" "user.invited.remove")}}

View File

@ -1,5 +1,5 @@
{{plugin-outlet name="above-user-profile" tagName="" args=(hash model=model)}}
<div class="container {{if viewingSelf "viewing-self"}} {{if model.profile_hidden "profile-hidden"}}">
<div class="container {{if viewingSelf "viewing-self"}} {{if model.profile_hidden "profile-hidden"}} {{primaryGroup}}">
{{#d-section class="user-main"}}
<section class="{{if collapsedInfo "collapsed-info"}} about {{if hasProfileBackgroundUrl "has-background" "no-background"}}" >
{{#unless collapsedInfo}}

View File

@ -95,6 +95,11 @@ export default class PostCooked {
return;
}
let siteSettings = this.decoratorHelper.widget.siteSettings;
if (siteSettings.disable_image_size_calculations) {
return;
}
const maxImageWidth = siteSettings.max_image_width;
const maxImageHeight = siteSettings.max_image_height;

View File

@ -561,11 +561,7 @@ export default createWidget("post-menu", {
const repliesButton = this.attachButton("replies", attrs);
if (repliesButton) {
if (!this.site.mobileView) {
postControls.push(repliesButton);
} else {
visibleButtons.splice(-1, 0, repliesButton);
}
postControls.push(repliesButton);
}
const extraPostControls = applyDecorators(

View File

@ -1,23 +1,42 @@
import RawHtml from "discourse/widgets/raw-html";
import { iconHTML } from "discourse-common/lib/icon-library";
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
import UserAction from "discourse/models/user-action";
import { ajax } from "discourse/lib/ajax";
import { createWidgetFrom } from "discourse/widgets/widget";
import { createWidget, createWidgetFrom } from "discourse/widgets/widget";
import { h } from "virtual-dom";
import { postUrl } from "discourse/lib/utilities";
import I18n from "I18n";
const ICON = "bookmark";
createWidget("no-quick-access-bookmarks", {
html() {
return h("div.empty-state", [
h("span.empty-state-title", I18n.t("user.no_bookmarks_title")),
h(
"div.empty-state-body",
new RawHtml({
html:
"<p>" +
I18n.t("user.no_bookmarks_body", {
icon: iconHTML(ICON),
}).htmlSafe() +
"</p>",
})
),
]);
},
});
createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
buildKey: () => "quick-access-bookmarks",
emptyStateWidget: "no-quick-access-bookmarks",
showAllHref() {
return `${this.attrs.path}/activity/bookmarks`;
},
emptyStatePlaceholderItem() {
return h("li.read", this.state.emptyStatePlaceholderItemText);
},
findNewItems() {
return this.loadBookmarksWithReminders();
},
@ -48,12 +67,6 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
cache: "false",
}).then((result) => {
result = result.user_bookmark_list;
// The empty state help text for bookmarks page is localized on the
// server.
if (result.no_results_help) {
this.state.emptyStatePlaceholderItemText = result.no_results_help;
}
return result.bookmarks;
});
},
@ -66,10 +79,7 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
filter: UserAction.TYPES.bookmarks,
no_results_help_key: "user_activity.no_bookmarks",
},
}).then(({ user_actions, no_results_help }) => {
// The empty state help text for bookmarks page is localized on the
// server.
this.state.emptyStatePlaceholderItemText = no_results_help;
}).then(({ user_actions }) => {
return user_actions;
});
},

View File

@ -1,10 +1,37 @@
import RawHtml from "discourse/widgets/raw-html";
import { iconHTML } from "discourse-common/lib/icon-library";
import getURL from "discourse-common/lib/get-url";
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
import { ajax } from "discourse/lib/ajax";
import { createWidgetFrom } from "discourse/widgets/widget";
import { createWidget, createWidgetFrom } from "discourse/widgets/widget";
import { h } from "virtual-dom";
import I18n from "I18n";
const ICON = "bell";
createWidget("no-quick-access-notifications", {
html() {
return h("div.empty-state", [
h("span.empty-state-title", I18n.t("user.no_notifications_title")),
h(
"div.empty-state-body",
new RawHtml({
html:
"<p>" +
I18n.t("user.no_notifications_body", {
preferencesUrl: getURL("/my/preferences/notifications"),
icon: iconHTML(ICON),
}).htmlSafe() +
"</p>",
})
),
]);
},
});
createWidgetFrom(QuickAccessPanel, "quick-access-notifications", {
buildKey: () => "quick-access-notifications",
emptyStatePlaceholderItemKey: "notifications.empty",
emptyStateWidget: "no-quick-access-notifications",
buildAttributes() {
return { tabindex: -1 };

View File

@ -79,7 +79,7 @@ export default createWidget("quick-access-panel", {
},
refreshNotifications(state) {
if (this.loading) {
if (state.loading) {
return;
}

View File

@ -129,6 +129,12 @@ createSearchResult({
userTitles.push(h("span.username", formatUsername(u.username)));
if (u.custom_data) {
u.custom_data.forEach((row) =>
userTitles.push(h("span.custom-field", `${row.name}: ${row.value}`))
);
}
const userResultContents = [
avatarImg("small", {
template: u.avatar_template,

View File

@ -8,6 +8,7 @@ const prettyTextEngine = require("./lib/pretty-text-engine");
const { createI18nTree } = require("./lib/translation-plugin");
const discourseScss = require("./lib/discourse-scss");
const funnel = require("broccoli-funnel");
const AssetRev = require("broccoli-asset-rev");
module.exports = function (defaults) {
let discourseRoot = resolve("../../../..");
@ -20,6 +21,13 @@ module.exports = function (defaults) {
},
});
// Ember CLI does this by default for the app tree, but for our extra bundles we
// need to do it ourselves in production mode.
const isProduction = EmberApp.env().includes("production");
function digest(tree) {
return isProduction ? new AssetRev(tree) : tree;
}
// WARNING: We should only import scripts here if they are not in NPM.
// For example: our very specific version of bootstrap-modal.
app.import(vendorJs + "bootbox.js");
@ -37,9 +45,11 @@ module.exports = function (defaults) {
files: ["highlight-test-bundle.min.js"],
destDir: "assets/highlightjs",
}),
concat(app.options.adminTree, {
outputFile: `assets/admin.js`,
}),
prettyTextEngine(vendorJs, "discourse-markdown"),
digest(
concat(app.options.adminTree, {
outputFile: `assets/admin.js`,
})
),
digest(prettyTextEngine(vendorJs, "discourse-markdown")),
]);
};

View File

@ -141,25 +141,65 @@ function applyBootstrap(bootstrap, template) {
return template;
}
function decorateIndex(assetPath, baseUrl, headers) {
function buildFromBootstrap(assetPath, proxy, req) {
// eslint-disable-next-line
return new Promise((resolve, reject) => {
fs.readFile(
path.join(process.cwd(), "dist", assetPath),
"utf8",
(err, template) => {
getJSON(`${baseUrl}/bootstrap.json`, null, headers)
getJSON(`${proxy}/bootstrap.json`, null, req.headers)
.then((json) => {
resolve(applyBootstrap(json.bootstrap, template));
})
.catch(() => {
reject(`Could not get ${baseUrl}/bootstrap.json`);
reject(`Could not get ${proxy}/bootstrap.json`);
});
}
);
});
}
async function handleRequest(assetPath, proxy, req, res) {
if (assetPath.endsWith("tests/index.html")) {
return;
}
if (assetPath.endsWith("index.html")) {
try {
// Avoid Ember CLI's proxy if doing a GET, since Discourse depends on some non-XHR
// GET requests to work.
if (req.method === "GET") {
let url = `${proxy}${req.path}`;
let queryLoc = req.url.indexOf("?");
if (queryLoc !== -1) {
url += req.url.substr(queryLoc);
}
req.headers["X-Discourse-Ember-CLI"] = "true";
let get = bent("GET", [200, 404, 403, 500]);
let response = await get(url, null, req.headers);
res.set(response.headers);
if (response.headers["x-discourse-bootstrap-required"] === "true") {
req.headers["X-Discourse-Asset-Path"] = req.path;
let json = await buildFromBootstrap(assetPath, proxy, req);
return res.send(json);
}
res.status(response.status);
res.send(await response.text());
}
} catch (e) {
res.send(`
<html>
<h1>Discourse Build Error</h1>
<p>${e.toString()}</p>
</html>
`);
}
}
}
module.exports = {
name: require("./package").name,
@ -172,6 +212,16 @@ module.exports = {
let app = config.app;
let options = config.options;
if (!proxy) {
// eslint-disable-next-line
console.error(`
Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application
to serve API requests. For example:
yarn run ember serve --proxy "http://localhost:3000"\n`);
throw "--proxy argument is required";
}
let watcher = options.watcher;
let baseURL =
@ -190,31 +240,17 @@ module.exports = {
isFile = fs
.statSync(path.join(results.directory, assetPath))
.isFile();
} catch (err) {
/* ignore */
}
} catch (err) {}
if (!isFile) {
assetPath = "index.html";
}
if (assetPath.endsWith("index.html")) {
let template;
try {
template = await decorateIndex(assetPath, proxy, req.headers);
} catch (e) {
template = `
<html>
<h1>Discourse Build Error</h1>
<p>${e.toString()}</p>
</html>
`;
}
return res.send(template);
}
await handleRequest(assetPath, proxy, req, res);
}
} finally {
next();
if (!res.headersSent) {
return next();
}
}
});
},
@ -233,6 +269,10 @@ module.exports = {
return false;
}
if (req.path.endsWith(".json")) {
return false;
}
let baseURL =
options.rootURL === ""
? "/"

View File

@ -67,8 +67,18 @@ acceptance("Admin - Themes - Install modal", function (needs) {
);
assert.ok(query(publicKey), "shows public key");
// Supports AWS CodeCommit style repo URLs
await fillIn(
urlInput,
"ssh://someID@git-codecommit.us-west-2.amazonaws.com/v1/repos/test-repo.git"
);
assert.ok(query(publicKey), "shows public key");
await fillIn(urlInput, "https://github.com/discourse/discourse.git");
assert.notOk(query(publicKey), "does not shows public key for https urls");
assert.notOk(query(publicKey), "does not show public key for https urls");
await fillIn(urlInput, "git@github.com:discourse/discourse.git");
assert.ok(query(publicKey), "shows public key for valid github repo url");
});
test("modal can be auto-opened with the right query params", async function (assert) {

View File

@ -1,7 +1,9 @@
import {
acceptance,
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import I18n from "I18n";
import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
import Category from "discourse/models/category";
import selectKit from "discourse/tests/helpers/select-kit-helper";
@ -50,6 +52,45 @@ acceptance("Composer - Tags", function (needs) {
await click("#reply-control button.create");
assert.equal(currentURL(), "/");
assert.equal(
queryAll(".popup-tip.bad").text().trim(),
I18n.t("composer.error.tags_missing", { count: 1 }),
"it should display the right alert"
);
const tags = selectKit(".mini-tag-chooser");
await tags.expand();
await tags.selectRowByValue("monkey");
await click("#reply-control button.create");
assert.notEqual(currentURL(), "/");
});
test("users do not bypass min required tags in tag group validation rule", async function (assert) {
await visit("/");
await click("#create-topic");
await fillIn("#reply-title", "this is my new topic title");
await fillIn(".d-editor-input", "this is the *content* of a post");
Category.findById(2).setProperties({
required_tag_groups: ["support tags"],
min_tags_from_required_group: 1,
});
const categoryChooser = selectKit(".category-chooser");
await categoryChooser.expand();
await categoryChooser.selectRowByValue(2);
updateCurrentUser({ moderator: false, admin: false, trust_level: 1 });
await click("#reply-control button.create");
assert.equal(currentURL(), "/");
assert.equal(
queryAll(".popup-tip.bad").text().trim(),
I18n.t("composer.error.tags_missing", { count: 1 }),
"it should display the right alert"
);
const tags = selectKit(".mini-tag-chooser");
await tags.expand();

View File

@ -29,6 +29,15 @@ acceptance("Composer", function (needs) {
server.get("/posts/419", () => {
return helper.response({ id: 419 });
});
server.get("/u/is_local_username", () => {
return helper.response({
valid: [],
valid_groups: ["staff"],
mentionable_groups: [{ name: "staff", user_count: 30 }],
cannot_see: [],
max_users_notified_per_group_mention: 100,
});
});
});
skip("Tests the Composer controls", async function (assert) {
@ -1007,4 +1016,18 @@ acceptance("Composer", function (needs) {
await fillIn(".d-editor-input", "[](https://github.com)");
assert.equal(find(".composer-popup").length, 1);
});
test("Shows the 'group_mentioned' notice", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .create");
await fillIn(".d-editor-input", "[quote]\n@staff\n[/quote]");
assert.notOk(
exists(".composer-popup"),
"Doesn't show the 'group_mentioned' notice in a quote"
);
await fillIn(".d-editor-input", "@staff");
assert.ok(exists(".composer-popup"), "Shows the 'group_mentioned' notice");
});
});

View File

@ -43,7 +43,7 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
"shows an invite link when modal is opened"
);
await click("#invite-show-advanced a");
await click(".modal-footer .show-advanced");
await assert.ok(
find(".invite-to-groups").length > 0,
"shows advanced options"
@ -57,7 +57,7 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
"shows advanced options"
);
await click(".modal-footer .btn:last-child");
await click(".modal-close");
assert.ok(deleted, "deletes the invite if not saved");
});
@ -77,7 +77,7 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
"adds invite to list after saving"
);
await click(".modal-footer .btn:last-child");
await click(".modal-close");
assert.notOk(deleted, "does not delete invite on close");
});
@ -85,9 +85,9 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child");
await click(".invite-link-field .btn");
await click(".invite-link .btn");
await click(".modal-footer .btn:last-child");
await click(".modal-close");
assert.notOk(deleted, "does not delete invite on close");
});
@ -95,8 +95,8 @@ acceptance("Invites - Create & Edit Invite Modal", function (needs) {
await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child");
await click("#invite-type-email");
await click(".invite-link-field .btn");
await click("#invite-type");
await click(".invite-link .btn");
assert.equal(
find("#modal-alert").text(),
I18n.t("user.invited.invite.blank_email")
@ -130,7 +130,6 @@ acceptance("Invites - Link Invites", function (needs) {
await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child");
await click("#invite-type-link");
assert.ok(
find("#invite-max-redemptions").length,
"shows max redemptions field"
@ -173,7 +172,7 @@ acceptance("Invites - Email Invites", function (needs) {
await visit("/u/eviltrout/invited/pending");
await click(".invite-controls .btn:first-child");
await click("#invite-type-email");
await click("#invite-type");
assert.ok(find("#invite-email").length, "shows email field");

View File

@ -1,96 +0,0 @@
import {
acceptance,
exists,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
acceptance("Share and Invite modal - desktop", function (needs) {
needs.user();
test("Topic footer button", async function (assert) {
await visit("/t/internationalization-localization/280");
assert.ok(
exists("#topic-footer-button-share-and-invite"),
"the button exists"
);
await click("#topic-footer-button-share-and-invite");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.is-active"),
"it activates the share tab by default"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.invite"),
"it shows the invite tab"
);
assert.equal(
queryAll(".share-and-invite.modal .modal-panel.share .title").text(),
"Topic: Internationalization / localization",
"it shows the topic title"
);
assert.ok(
queryAll(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("/t/internationalization-localization/280?u=eviltrout"),
"it shows the topic sharing url"
);
assert.ok(
queryAll(".share-and-invite.modal .social-link").length > 1,
"it shows social sources"
);
await click(".share-and-invite.modal .modal-tab.invite");
assert.ok(
exists(
".share-and-invite.modal .modal-panel.invite .send-invite:disabled"
),
"send invite button is disabled"
);
assert.ok(
exists(
".share-and-invite.modal .modal-panel.invite .generate-invite-link:disabled"
),
"generate invite button is disabled"
);
});
test("Post date link", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
assert.ok(exists("#share-link"), "it shows the share modal");
});
});
acceptance("Share url with badges disabled - desktop", function (needs) {
needs.user();
needs.settings({ enable_badges: false });
test("topic footer button - badges disabled - desktop", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-share-and-invite");
assert.notOk(
queryAll(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("?u=eviltrout"),
"it doesn't add the username param when badges are disabled"
);
});
});

View File

@ -1,12 +1,55 @@
import { click, visit } from "@ember/test-helpers";
import {
acceptance,
exists,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { click, visit } from "@ember/test-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import { test } from "qunit";
acceptance("Share and Invite modal", function (needs) {
needs.user();
test("Topic footer button", async function (assert) {
await visit("/t/internationalization-localization/280");
assert.ok(
exists("#topic-footer-button-share-and-invite"),
"the button exists"
);
await click("#topic-footer-button-share-and-invite");
assert.ok(exists(".share-topic-modal"), "it shows the modal");
assert.ok(
queryAll("input.invite-link")
.val()
.includes("/t/internationalization-localization/280?u=eviltrout"),
"it shows the topic sharing url"
);
assert.ok(queryAll(".social-link").length > 1, "it shows social sources");
assert.ok(
exists(".btn-primary[aria-label='Notify']"),
"it shows the notify button"
);
assert.ok(
exists(".btn-primary[aria-label='Invite']"),
"it shows the invite button"
);
});
test("Post date link", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
assert.ok(exists("#share-link"), "it shows the share modal");
});
});
acceptance("Share and Invite modal - mobile", function (needs) {
needs.user();
needs.mobileView();
@ -23,67 +66,19 @@ acceptance("Share and Invite modal - mobile", function (needs) {
await subject.expand();
await subject.selectRowByValue("share-and-invite");
assert.ok(exists(".share-and-invite.modal"), "it shows the modal");
assert.ok(
exists(".share-and-invite.modal .modal-tab.share"),
"it shows the share tab"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.share.is-active"),
"it activates the share tab by default"
);
assert.ok(
exists(".share-and-invite.modal .modal-tab.invite"),
"it shows the invite tab"
);
assert.equal(
queryAll(".share-and-invite.modal .modal-panel.share .title").text(),
"Topic: Internationalization / localization",
"it shows the topic title"
);
assert.ok(
queryAll(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("/t/internationalization-localization/280?u=eviltrout"),
"it shows the topic sharing url"
);
assert.ok(
queryAll(".share-and-invite.modal .social-link").length > 1,
"it shows social sources"
);
});
test("Post date link", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#post_2 .post-info.post-date a");
assert.ok(exists("#share-link"), "it shows the share modal");
assert.ok(exists(".share-topic-modal"), "it shows the modal");
});
});
acceptance("Share url with badges disabled - mobile", function (needs) {
acceptance("Share url with badges disabled - desktop", function (needs) {
needs.user();
needs.mobileView();
needs.settings({
enable_badges: false,
});
test("topic footer button - badges disabled - mobile", async function (assert) {
needs.settings({ enable_badges: false });
test("topic footer button - badges disabled - desktop", async function (assert) {
await visit("/t/internationalization-localization/280");
const subject = selectKit(".topic-footer-mobile-dropdown");
await subject.expand();
await subject.selectRowByValue("share-and-invite");
await click("#topic-footer-button-share-and-invite");
assert.notOk(
queryAll(".share-and-invite.modal .modal-panel.share .topic-share-url")
.val()
.includes("?u=eviltrout"),
queryAll("input.invite-link").val().includes("?u=eviltrout"),
"it doesn't add the username param when badges are disabled"
);
});

View File

@ -66,8 +66,6 @@ acceptance("Tag Groups", function (needs) {
await tags.selectRowByValue("monkey");
await click("#visible-permission");
assert.ok(queryAll(".tag-group-content .btn.btn-default:disabled").length);
await groups.expand();
await groups.selectRowByIndex(1);
await groups.selectRowByIndex(0);

View File

@ -133,6 +133,41 @@ acceptance("Topic - Edit timer", function (needs) {
assert.ok(regex.test(text));
});
test("schedule - last custom date and time", async function (assert) {
updateCurrentUser({ moderator: true });
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".admin-topic-timer-update button");
await click("#tap_tile_custom");
await click(".modal-close");
await click(".toggle-admin-menu");
await click(".admin-topic-timer-update button");
assert.notOk(
exists("#tap_tile_last_custom"),
"it does not show last custom if the custom date and time was not filled and valid"
);
await click("#tap_tile_custom");
await fillIn(".tap-tile-date-input .date-picker", "2099-11-24");
await fillIn("#custom-time", "10:30");
await click(".edit-topic-timer-buttons button.btn-primary");
await click(".toggle-admin-menu");
await click(".admin-topic-timer-update button");
assert.ok(
exists("#tap_tile_last_custom"),
"it show last custom because the custom date and time was valid"
);
let text = queryAll("#tap_tile_last_custom").text().trim();
const regex = /Nov 24, 10:30 am/g;
assert.ok(regex.test(text));
});
test("TL4 can't auto-delete", async function (assert) {
updateCurrentUser({ moderator: false, admin: false, trust_level: 4 });

View File

@ -26,8 +26,8 @@ acceptance("Category 404", function (needs) {
acceptance("Unknown", function (needs) {
const urls = {
"viewtopic.php": "/t/internationalization-localization/280",
"not-the-url-for-faq": "/faq",
"/viewtopic.php?f=8&t=280": "/t/internationalization-localization/280",
"/another-url-for-faq": "/faq",
};
needs.pretender((server, helper) => {
@ -59,7 +59,7 @@ acceptance("Unknown", function (needs) {
});
test("Permalink URL to a static page", async function (assert) {
await visit("/not-the-url-for-faq");
await visit("/another-url-for-faq");
assert.equal(currentURL(), "/faq");
});
});

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