Version bump
This commit is contained in:
commit
992587f836
2
.tool-versions
Normal file
2
.tool-versions
Normal file
@ -0,0 +1,2 @@
|
||||
nodejs 15.14.0
|
||||
ruby 2.7.2
|
||||
14
Gemfile
14
Gemfile
@ -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
|
||||
|
||||
|
||||
118
Gemfile.lock
118
Gemfile.lock
@ -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
|
||||
|
||||
4
Rakefile
4
Rakefile
@ -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?
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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"
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -278,6 +278,7 @@ const Report = EmberObject.extend({
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
htmlTitle: label.html_title,
|
||||
sortProperty: label.sort_property || mainProperty,
|
||||
mainProperty,
|
||||
type,
|
||||
|
||||
@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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}`),
|
||||
|
||||
116
app/assets/javascripts/discourse/app/controllers/share-topic.js
Normal file
116
app/assets/javascripts/discourse/app/controllers/share-topic.js
Normal 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 });
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -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");
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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" }
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -74,7 +74,8 @@ export default {
|
||||
topicTrackingState,
|
||||
siteSettings,
|
||||
session,
|
||||
currentUser
|
||||
currentUser,
|
||||
app.appEvents
|
||||
);
|
||||
app.register("service:screen-track", screenTrack, { instantiate: false });
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -22,7 +22,9 @@
|
||||
translatedTitle=b.title
|
||||
label=b.label
|
||||
icon=b.icon
|
||||
class=b.className}}
|
||||
class=b.className
|
||||
preventFocus=b.preventFocus
|
||||
}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
|
||||
@ -6,4 +6,6 @@
|
||||
input=(action "onChangeDate")
|
||||
}}
|
||||
|
||||
<div class="picker-container"></div>
|
||||
{{#unless useGlobalPickerContainer}}
|
||||
<div class="picker-container"></div>
|
||||
{{/unless}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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)}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}}
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "save")
|
||||
disabled=savingDisabled
|
||||
disabled=buffered.isSaving
|
||||
label="tagging.groups.save"}}
|
||||
|
||||
{{d-button
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -94,6 +94,7 @@
|
||||
options=(hash
|
||||
categoryId=model.categoryId
|
||||
minimum=model.minimumRequiredTags
|
||||
requiredTagGroups=model.requiredTagGroups
|
||||
)
|
||||
}}
|
||||
{{popup-input-tip validation=tagValidation}}
|
||||
|
||||
@ -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)}}
|
||||
|
||||
@ -102,3 +102,5 @@
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{/d-section}}
|
||||
|
||||
{{plugin-outlet name="after-groups-index-container" tagName=""}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}}
|
||||
—
|
||||
{{/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}}
|
||||
—
|
||||
{{/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")}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -79,7 +79,7 @@ export default createWidget("quick-access-panel", {
|
||||
},
|
||||
|
||||
refreshNotifications(state) {
|
||||
if (this.loading) {
|
||||
if (state.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")),
|
||||
]);
|
||||
};
|
||||
|
||||
@ -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 === ""
|
||||
? "/"
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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"
|
||||
);
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
|
||||
|
||||
@ -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
Reference in New Issue
Block a user