diff --git a/.eslintignore b/.eslintignore
index 3956d17380..a07eade1d2 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -16,3 +16,4 @@ app/assets/javascripts/discourse/tests/test-boot-rails.js
app/assets/javascripts/discourse/tests/fixtures
node_modules/
dist/
+tmp/
diff --git a/.github/workflows/ember_with_plugins.yml b/.github/workflows/ember_with_plugins.yml
index ecb934bb56..52b4277415 100644
--- a/.github/workflows/ember_with_plugins.yml
+++ b/.github/workflows/ember_with_plugins.yml
@@ -26,7 +26,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index ef6906dd78..e1187dcffa 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -28,7 +28,7 @@ jobs:
git config --global user.name "Discourse CI"
- name: Bundler cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -49,7 +49,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9af681a4bd..fc76e8c78b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -63,7 +63,7 @@ jobs:
sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';"
- name: Bundler cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -84,7 +84,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
@@ -104,7 +104,7 @@ jobs:
run: bin/rake plugin:pull_compatible_all
- name: Fetch app state cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: app-cache
with:
path: tmp/app-cache
@@ -216,7 +216,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
@@ -235,16 +235,19 @@ jobs:
sudo -E -u discourse -H yarn ember build --environment=test -o /tmp/emberbuild
- name: Core QUnit 1
+ if: ${{ always() }}
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
- name: Core QUnit 2
+ if: ${{ always() }}
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
- name: Core QUnit 3
+ if: ${{ always() }}
working-directory: ./app/assets/javascripts/discourse
run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" --random
timeout-minutes: 20
diff --git a/Gemfile b/Gemfile
index c66a3f2554..2766c3fead 100644
--- a/Gemfile
+++ b/Gemfile
@@ -31,9 +31,7 @@ end
gem 'json'
-# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
-# This is a desired upgrade we should get to.
-gem 'sprockets', '3.7.2'
+gem 'sprockets'
# this will eventually be added to rails,
# allows us to precompile all our templates in the unicorn master
diff --git a/Gemfile.lock b/Gemfile.lock
index 35eec5d987..9de187f37d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -92,7 +92,7 @@ GEM
chunky_png (1.4.0)
coderay (1.1.3)
colored2 (3.1.2)
- concurrent-ruby (1.1.9)
+ concurrent-ruby (1.1.10)
connection_pool (2.2.5)
cose (1.2.0)
cbor (~> 0.5.9)
@@ -129,10 +129,10 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
- excon (0.92.1)
+ excon (0.92.2)
execjs (2.8.1)
exifr (1.3.9)
- fabrication (2.27.0)
+ fabrication (2.28.0)
faker (2.20.0)
i18n (>= 1.8.11, < 2)
fakeweb (1.3.0)
@@ -194,7 +194,7 @@ GEM
json (2.6.1)
json-schema (2.8.1)
addressable (>= 2.4)
- json_schemer (0.2.19)
+ json_schemer (0.2.20)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
@@ -211,7 +211,7 @@ GEM
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
literate_randomizer (0.4.0)
- lograge (0.11.2)
+ lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@@ -220,7 +220,7 @@ GEM
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.11.0)
- loofah (2.15.0)
+ loofah (2.16.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@@ -241,7 +241,7 @@ GEM
ffi (~> 1.9)
minitest (5.15.0)
mocha (1.13.0)
- msgpack (1.4.5)
+ msgpack (1.5.1)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
@@ -293,12 +293,12 @@ GEM
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
optimist (3.0.1)
- parallel (1.22.0)
- parallel_tests (3.7.3)
+ parallel (1.22.1)
+ parallel_tests (3.8.1)
parallel
- parser (3.1.1.0)
+ parser (3.1.2.0)
ast (~> 2.4.1)
- pg (1.3.4)
+ pg (1.3.5)
progress (3.6.0)
pry (0.13.1)
coderay (~> 1.1)
@@ -308,8 +308,8 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
- public_suffix (4.0.6)
- puma (5.6.2)
+ public_suffix (4.0.7)
+ puma (5.6.4)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.6.0)
@@ -352,7 +352,7 @@ GEM
redis (4.5.1)
redis-namespace (1.8.2)
redis (>= 3.0.4)
- regexp_parser (2.2.1)
+ regexp_parser (2.3.0)
request_store (1.5.1)
rack (>= 1.4)
rexml (3.2.5)
@@ -374,7 +374,7 @@ GEM
rspec-html-matchers (0.9.4)
nokogiri (~> 1)
rspec (>= 3.0.0.a, < 4)
- rspec-mocks (3.11.0)
+ rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-rails (5.1.1)
@@ -393,7 +393,7 @@ GEM
json-schema (~> 2.2)
railties (>= 3.1, < 7.1)
rtlit (0.0.5)
- rubocop (1.26.0)
+ rubocop (1.27.0)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
@@ -402,7 +402,7 @@ GEM
rubocop-ast (>= 1.16.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.16.0)
+ rubocop-ast (1.17.0)
parser (>= 3.1.1.0)
rubocop-discourse (2.5.0)
rubocop (>= 1.1.0)
@@ -443,7 +443,7 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
- sprockets (3.7.2)
+ sprockets (4.0.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@@ -452,7 +452,7 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.19)
- test-prof (1.0.7)
+ test-prof (1.0.8)
thor (1.2.1)
tilt (2.0.10)
tzinfo (2.0.4)
@@ -466,7 +466,7 @@ GEM
unicorn (6.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
- uniform_notifier (1.15.0)
+ uniform_notifier (1.16.0)
uri_template (0.7.0)
webmock (3.14.0)
addressable (>= 2.8.0)
@@ -602,7 +602,7 @@ DEPENDENCIES
shoulda-matchers
sidekiq
simplecov
- sprockets (= 3.7.2)
+ sprockets
sprockets-rails
sshkey
stackprof
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
new file mode 100644
index 0000000000..ac907b3677
--- /dev/null
+++ b/app/assets/config/manifest.js
@@ -0,0 +1 @@
+//= link_tree ../images
diff --git a/app/assets/javascripts/admin/addon/components/site-settings/color.js b/app/assets/javascripts/admin/addon/components/site-settings/color.js
index 8d098cb7d8..0b1db9a5c1 100644
--- a/app/assets/javascripts/admin/addon/components/site-settings/color.js
+++ b/app/assets/javascripts/admin/addon/components/site-settings/color.js
@@ -5,7 +5,7 @@ function RGBToHex(rgb) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " ";
// Turn "rgb(r,g,b)" into [r,g,b]
- rgb = rgb.substr(4).split(")")[0].split(sep);
+ rgb = rgb.slice(4).split(")")[0].split(sep);
let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js
index dc5ba1193d..3dbcc3742c 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js
@@ -30,7 +30,7 @@ export default Controller.extend({
}
if (word.startsWith("plugin:")) {
- pluginFilter = word.substr("plugin:".length).trim();
+ pluginFilter = word.slice("plugin:".length).trim();
return false;
}
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
index c11df41a5f..807bb39932 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
@@ -222,11 +222,6 @@ export default Controller.extend(CanCheckEmails, {
.then((result) => {
if (result.email_confirmation_required) {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
- } else {
- const controller = showModal("grant-admin-second-factor", {
- model: this.model,
- });
- controller.setResult(result);
}
})
.catch((error) => {
diff --git a/app/assets/javascripts/admin/addon/models/color-scheme-color.js b/app/assets/javascripts/admin/addon/models/color-scheme-color.js
index 49cb552a92..24beb9130c 100644
--- a/app/assets/javascripts/admin/addon/models/color-scheme-color.js
+++ b/app/assets/javascripts/admin/addon/models/color-scheme-color.js
@@ -76,17 +76,17 @@ const ColorSchemeColor = EmberObject.extend({
if (hex.length === 6 || hex.length === 3) {
if (hex.length === 3) {
hex =
- hex.substr(0, 1) +
- hex.substr(0, 1) +
- hex.substr(1, 1) +
- hex.substr(1, 1) +
- hex.substr(2, 1) +
- hex.substr(2, 1);
+ hex.slice(0, 1) +
+ hex.slice(0, 1) +
+ hex.slice(1, 2) +
+ hex.slice(1, 2) +
+ hex.slice(2, 3) +
+ hex.slice(2, 3);
}
return Math.round(
- (parseInt(hex.substr(0, 2), 16) * 299 +
- parseInt(hex.substr(2, 2), 16) * 587 +
- parseInt(hex.substr(4, 2), 16) * 114) /
+ (parseInt(hex.slice(0, 2), 16) * 299 +
+ parseInt(hex.slice(2, 4), 16) * 587 +
+ parseInt(hex.slice(4, 6), 16) * 114) /
1000
);
}
diff --git a/app/assets/javascripts/admin/addon/models/version-check.js b/app/assets/javascripts/admin/addon/models/version-check.js
index 4f939064af..edf6ad5e0e 100644
--- a/app/assets/javascripts/admin/addon/models/version-check.js
+++ b/app/assets/javascripts/admin/addon/models/version-check.js
@@ -28,7 +28,7 @@ const VersionCheck = EmberObject.extend({
@discourseComputed("installed_sha")
shortSha(installedSHA) {
if (installedSHA) {
- return installedSHA.substr(0, 10);
+ return installedSHA.slice(0, 10);
}
},
});
diff --git a/app/assets/javascripts/admin/addon/templates/badges-show.hbs b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
index a1260d0481..524a53c812 100644
--- a/app/assets/javascripts/admin/addon/templates/badges-show.hbs
+++ b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
@@ -68,7 +68,9 @@
value=buffered.badge_type_id
content=badgeTypes
onChange=(action (mut buffered.badge_type_id))
- isDisabled=readOnly
+ options=(hash
+ disabled=readOnly
+ )
}}
@@ -155,7 +157,9 @@
value=buffered.trigger
content=badgeTriggers
onChange=(action (mut buffered.trigger))
- disabled=readOnly
+ options=(hash
+ disabled=readOnly
+ )
}}
{{/if}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs
index bac1facc69..c306bcc68f 100644
--- a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs
@@ -1,11 +1,11 @@
{{combo-box
- filterable=true
valueProperty="value"
content=groupOptions
value=groupId
- none="admin.dashboard.reports.groups"
onChange=(action "onChange")
options=(hash
allowAny=filter.allow_any
+ filterable=true
+ none="admin.dashboard.reports.groups"
)
}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs b/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs
index 88d71d0747..e01c74268b 100644
--- a/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs
@@ -1,8 +1,10 @@
{{combo-box
content=filter.choices
- filterable=true
- allowAny=filter.allow_any
value=filter.default
- none="admin.dashboard.report_filter_any"
onChange=(action "onChange")
+ options=(hash
+ allowAny=filter.allow_any
+ filterable=true
+ none="admin.dashboard.report_filter_any"
+ )
}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs b/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs
index e2545c6863..27a80e7aaa 100644
--- a/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs
@@ -1,8 +1,8 @@
{{category-chooser
value=value
- allowUncategorized=true
onChange=(action (mut value))
options=(hash
+ allowUncategorized=true
none=(eq setting.default "")
)
}}
diff --git a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs
index 518922c965..c9852ed170 100644
--- a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs
@@ -34,13 +34,13 @@
{{/if}}
{{combo-box
- options=(hash
- allowAny=true
- )
- none=noneKey
valueProperty=null
nameProperty=null
value=newValue
content=filteredChoices
onChange=(action "selectChoice")
+ options=(hash
+ allowAny=true
+ none=noneKey
+ )
}}
diff --git a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs
index ee702a56d5..811510fa3e 100644
--- a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs
+++ b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs
@@ -159,10 +159,12 @@
{{color-palettes
content=colorSchemes
- filterable=true
- forceEscape=true
value=colorSchemeId
- icon="paint-brush"}}
+ icon="paint-brush"
+ options=(hash
+ filterable=true
+ )
+ }}
{{i18n "admin.customize.theme.color_scheme_select"}}
diff --git a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs
index e1c995437c..5677cc4664 100644
--- a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs
+++ b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs
@@ -5,7 +5,7 @@
{{i18n "admin.email.time"}} |
{{i18n "admin.email.user"}} |
{{i18n "admin.email.to_address"}} |
- {{i18n "admin.email.email_type"}} |
+ {{i18n "admin.email.email_type"}} |
@@ -13,7 +13,7 @@
{{i18n "admin.email.logs.filters.title"}} |
{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}} |
{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}} |
- {{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}} |
+ {{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}} |
{{#each model as |l|}}
@@ -28,15 +28,26 @@
{{/if}}
{{l.to_address}} |
- {{#if l.has_bounce_key}}
- {{l.email_type}} |
- {{else}}
- {{l.email_type}} |
- {{/if}}
+
+ {{#if l.has_bounce_key}}
+
+ {{l.email_type}}
+
+ {{else}}
+ {{l.email_type}}
+ {{/if}}
+ |
+
+ {{#if l.has_bounce_key}}
+
+ {{d-icon "info-circle"}}
+
+ {{/if}}
+ |
{{else}}
{{#unless loading}}
- | {{i18n "admin.email.logs.none"}} |
+ | {{i18n "admin.email.logs.none"}} |
{{/unless}}
{{/each}}
diff --git a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs
index 2338af8d15..c214a57e37 100644
--- a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs
+++ b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs
@@ -6,7 +6,7 @@
{{i18n "admin.email.incoming_emails.from_address"}} |
{{i18n "admin.email.incoming_emails.to_addresses"}} |
{{i18n "admin.email.incoming_emails.subject"}} |
- {{i18n "admin.email.incoming_emails.error"}} |
+ {{i18n "admin.email.incoming_emails.error"}} |
@@ -16,7 +16,7 @@
{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}} |
{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}} |
{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}} |
- {{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}} |
+ {{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}} |
{{#each model as |email|}}
@@ -50,9 +50,14 @@
{{email.error}}
|
+
+
+ {{d-icon "info-circle"}}
+
+ |
{{else}}
- | {{i18n "admin.email.incoming_emails.none"}} |
+ | {{i18n "admin.email.incoming_emails.none"}} |
{{/each}}
diff --git a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs
index 4213160362..afb605991b 100644
--- a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs
+++ b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs
@@ -34,8 +34,10 @@
{{combo-box
content=userHistoryActions
value=filterActionId
- none="admin.logs.staff_actions.all"
onChange=(action "filterActionIdChanged")
+ options=(hash
+ none="admin.logs.staff_actions.all"
+ )
}}
{{/if}}
diff --git a/app/assets/javascripts/admin/addon/templates/user-badges.hbs b/app/assets/javascripts/admin/addon/templates/user-badges.hbs
index 1b401b7e42..44549f6613 100644
--- a/app/assets/javascripts/admin/addon/templates/user-badges.hbs
+++ b/app/assets/javascripts/admin/addon/templates/user-badges.hbs
@@ -17,10 +17,12 @@
{{combo-box
- filterable=true
value=selectedBadgeId
content=grantableBadges
onChange=(action (mut selectedBadgeId))
+ options=(hash
+ filterable=true
+ )
}}
diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs
index ad93faf07d..643a5bb031 100644
--- a/app/assets/javascripts/admin/addon/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs
@@ -551,8 +551,10 @@
{{combo-box
content=model.customGroups
value=model.primary_group_id
- none="admin.groups.no_primary"
onChange=(action (mut model.primary_group_id))
+ options=(hash
+ none="admin.groups.no_primary"
+ )
}}
{{#if primaryGroupDirty}}
@@ -732,7 +734,7 @@
{{#if model.active}}
{{#if model.can_impersonate}}
{{d-button
- class="btn-danger"
+ class="btn-danger btn-impersonate"
action=(action "impersonate")
icon="crosshairs"
label="admin.impersonate.title"
@@ -743,21 +745,21 @@
{{#if model.can_be_anonymized}}
{{d-button label="admin.user.anonymize"
icon="exclamation-triangle"
- class="btn-danger"
+ class="btn-danger btn-anonymize"
action=(action "anonymize")}}
{{/if}}
{{#if model.canBeDeleted}}
{{d-button label="admin.user.delete"
icon="trash-alt"
- class="btn-danger"
+ class="btn-danger btn-user-delete"
action=(action "destroy")}}
{{/if}}
{{#if model.can_be_merged}}
{{d-button label="admin.user.merge.button"
icon="arrows-alt-h"
- class="btn-danger"
+ class="btn-danger btn-user-merge"
action=(action "promptTargetUser")}}
{{/if}}
diff --git a/app/assets/javascripts/browser-detect.js b/app/assets/javascripts/browser-detect.js
index d8d0eda481..3b93c14499 100644
--- a/app/assets/javascripts/browser-detect.js
+++ b/app/assets/javascripts/browser-detect.js
@@ -1,9 +1,4 @@
-if (
- !window.WeakMap ||
- !window.Promise ||
- typeof globalThis === "undefined" ||
- !String.prototype.replaceAll
-) {
+if (!window.WeakMap || !window.Promise || typeof globalThis === "undefined") {
window.unsupportedBrowser = true;
} else {
// Some implementations of `WeakMap.prototype.has` do not accept false
diff --git a/app/assets/javascripts/browser-update.js b/app/assets/javascripts/browser-update.js
index 07b5c9b5f4..31a5b410f6 100644
--- a/app/assets/javascripts/browser-update.js
+++ b/app/assets/javascripts/browser-update.js
@@ -29,11 +29,16 @@
// find the element with the "data-path" attribute set
for (var i = 0; i < noscriptElements.length; ++i) {
if (noscriptElements[i].getAttribute("data-path")) {
- // noscriptElements[i].innerHTML contains encoded HTML
- if (noscriptElements[i].childNodes.length > 0) {
- mainElement.innerHTML = noscriptElements[i].childNodes[0].nodeValue;
- break;
+ // noscriptElements[i].innerHTML contains encoded HTML, so we need to access
+ // the childNodes instead. Browsers seem to split very long content into multiple
+ // text childNodes.
+ var result = "";
+ for (var j = 0; j < noscriptElements[i].childNodes.length; j++) {
+ result += noscriptElements[i].childNodes[j].nodeValue;
}
+
+ mainElement.outerHTML = result;
+ break;
}
}
diff --git a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js
index d60b94eaf7..b66172c73b 100644
--- a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js
+++ b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js
@@ -33,7 +33,7 @@ AttributeHook.prototype.unhook = function (node, prop, next) {
}
let colonPosition = prop.indexOf(":");
- let localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop;
+ let localName = colonPosition > -1 ? prop.slice(colonPosition + 1) : prop;
node.removeAttributeNS(this.namespace, localName);
};
diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-url.js b/app/assets/javascripts/discourse-common/addon/lib/get-url.js
index 4a5a0f1715..fab9081c6f 100644
--- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js
+++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js
@@ -41,7 +41,7 @@ export function getURLWithCDN(url) {
}
export function getAbsoluteURL(path) {
- return baseUrl + path;
+ return baseUrl + withoutPrefix(path);
}
export function isAbsoluteURL(url) {
diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json
index 68f1a4c629..ebf77fa1e3 100644
--- a/app/assets/javascripts/discourse-common/package.json
+++ b/app/assets/javascripts/discourse-common/package.json
@@ -15,12 +15,12 @@
"start": "ember serve"
},
"dependencies": {
- "@uppy/aws-s3": "^2.0.4",
- "@uppy/aws-s3-multipart": "^2.1.0",
- "@uppy/core": "^2.1.0",
- "@uppy/drop-target": "^1.1.0",
- "@uppy/utils": "^4.0.3",
- "@uppy/xhr-upload": "^2.0.4",
+ "@uppy/aws-s3": "^2.0.8",
+ "@uppy/aws-s3-multipart": "^2.2.1",
+ "@uppy/core": "^2.1.6",
+ "@uppy/drop-target": "^1.1.2",
+ "@uppy/utils": "^4.0.5",
+ "@uppy/xhr-upload": "^2.0.7",
"ember-auto-import": "^2.2.4",
"ember-cli-babel": "^7.13.0",
"ember-cli-htmlbars": "^4.2.0",
diff --git a/app/assets/javascripts/discourse/app/components/bookmark-list.js b/app/assets/javascripts/discourse/app/components/bookmark-list.js
index 066a9019cd..9559c87a7e 100644
--- a/app/assets/javascripts/discourse/app/components/bookmark-list.js
+++ b/app/assets/javascripts/discourse/app/components/bookmark-list.js
@@ -93,19 +93,23 @@ export default Component.extend(Scrolling, {
@action
editBookmark(bookmark) {
- openBookmarkModal(bookmark, {
- onAfterSave: (savedData) => {
- this.appEvents.trigger(
- "bookmarks:changed",
- savedData,
- bookmark.attachedTo()
- );
- this.reload();
+ openBookmarkModal(
+ bookmark,
+ {
+ onAfterSave: (savedData) => {
+ this.appEvents.trigger(
+ "bookmarks:changed",
+ savedData,
+ bookmark.attachedTo()
+ );
+ this.reload();
+ },
+ onAfterDelete: () => {
+ this.reload();
+ },
},
- onAfterDelete: () => {
- this.reload();
- },
- });
+ { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks }
+ );
},
@action
diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js
index 8aa1dafb80..df1409a793 100644
--- a/app/assets/javascripts/discourse/app/components/bookmark.js
+++ b/app/assets/javascripts/discourse/app/components/bookmark.js
@@ -5,7 +5,10 @@ import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
-import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
+import {
+ TIME_SHORTCUT_TYPES,
+ defaultTimeShortcuts,
+} from "discourse/lib/time-shortcut";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
@@ -149,12 +152,19 @@ export default Component.extend({
const data = {
reminder_at: reminderAtISO,
name: this.model.name,
- post_id: this.model.postId,
id: this.model.id,
auto_delete_preference: this.autoDeletePreference,
- for_topic: this.model.forTopic,
};
+ if (this.siteSettings.use_polymorphic_bookmarks) {
+ data.bookmarkable_id = this.model.bookmarkableId;
+ data.bookmarkable_type = this.model.bookmarkableType;
+ } else {
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ data.post_id = this.model.postId;
+ data.for_topic = this.model.forTopic;
+ }
+
if (this.editingExistingBookmark) {
return ajax(`/bookmarks/${this.model.id}`, {
type: "PUT",
@@ -173,15 +183,25 @@ export default Component.extend({
if (!this.afterSave) {
return;
}
- this.afterSave({
+
+ const data = {
reminder_at: reminderAtISO,
- for_topic: this.model.forTopic,
auto_delete_preference: this.autoDeletePreference,
- post_id: this.model.postId,
id: this.model.id || response.id,
name: this.model.name,
- topic_id: this.model.topicId,
- });
+ };
+
+ if (this.siteSettings.use_polymorphic_bookmarks) {
+ data.bookmarkable_id = this.model.bookmarkableId;
+ data.bookmarkable_type = this.model.bookmarkableType;
+ } else {
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ data.post_id = this.model.postId;
+ data.for_topic = this.model.forTopic;
+ data.topic_id = this.model.topicId;
+ }
+
+ this.afterSave(data);
},
_deleteBookmark() {
@@ -274,12 +294,12 @@ export default Component.extend({
});
},
- @discourseComputed()
- customTimeShortcutOptions() {
- let customOptions = [];
+ @discourseComputed("userTimezone")
+ timeOptions(userTimezone) {
+ const options = defaultTimeShortcuts(userTimezone);
if (this.showPostLocalDate) {
- customOptions.push({
+ options.push({
icon: "globe-americas",
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
label: "time_shortcut.post_local_date",
@@ -289,7 +309,7 @@ export default Component.extend({
});
}
- return customOptions;
+ return options;
},
@discourseComputed("existingBookmarkHasReminder")
diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index eed18fe3b1..f836b7ff78 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -1,8 +1,4 @@
-import {
- authorizedExtensions,
- authorizesAllExtensions,
- authorizesOneOrMoreImageExtensions,
-} from "discourse/lib/uploads";
+import { authorizesOneOrMoreImageExtensions } from "discourse/lib/uploads";
import { alias } from "@ember/object/computed";
import { BasePlugin } from "@uppy/core";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
@@ -103,7 +99,10 @@ export default Component.extend(ComposerUploadUppy, {
editorClass: ".d-editor",
fileUploadElementId: "file-uploader",
mobileFileUploaderId: "mobile-file-upload",
+
+ // TODO (martin) Remove this once the chat plugin is using the new composerEventPrefix
eventPrefix: "composer",
+ composerEventPrefix: "composer",
uploadType: "composer",
uppyId: "composer-editor-uppy",
composerModel: alias("composer"),
@@ -198,24 +197,6 @@ export default Component.extend(ComposerUploadUppy, {
});
},
- @discourseComputed()
- acceptsAllFormats() {
- return (
- this.capabilities.isIOS ||
- authorizesAllExtensions(this.currentUser.staff, this.siteSettings)
- );
- },
-
- @discourseComputed()
- acceptedFormats() {
- const extensions = authorizedExtensions(
- this.currentUser.staff,
- this.siteSettings
- );
-
- return extensions.map((ext) => `.${ext}`).join();
- },
-
@bind
_afterMentionComplete(value) {
this.composer.set("reply", value);
diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js
index ad323bcb55..2f00c76a49 100644
--- a/app/assets/javascripts/discourse/app/components/composer-messages.js
+++ b/app/assets/javascripts/discourse/app/components/composer-messages.js
@@ -158,7 +158,7 @@ export default Component.extend({
}
// TODO pass the 200 in from somewhere
- const raw = (composer.get("reply") || "").substr(0, 200);
+ const raw = (composer.get("reply") || "").slice(0, 200);
const title = composer.get("title") || "";
// Ensure we have at least a title
diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js
index 8e86af03e4..afae9e0428 100644
--- a/app/assets/javascripts/discourse/app/components/d-editor.js
+++ b/app/assets/javascripts/discourse/app/components/d-editor.js
@@ -576,11 +576,15 @@ export default Component.extend(TextareaTextManipulation, {
return resolve(options);
})
- .then((list) =>
- list.map((code) => {
+ .then((list) => {
+ if (list === SKIP) {
+ return [];
+ }
+
+ return list.map((code) => {
return { code, src: emojiUrlFor(code) };
- })
- )
+ });
+ })
.then((list) => {
if (list.length) {
list.push({ label: I18n.t("composer.more_emoji"), term });
diff --git a/app/assets/javascripts/discourse/app/components/edit-category-tags.js b/app/assets/javascripts/discourse/app/components/edit-category-tags.js
index f9dc2d914b..b12a980ed7 100644
--- a/app/assets/javascripts/discourse/app/components/edit-category-tags.js
+++ b/app/assets/javascripts/discourse/app/components/edit-category-tags.js
@@ -1,8 +1,29 @@
import { and, empty } from "@ember/object/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
+import { action, set } from "@ember/object";
export default buildCategoryPanel("tags", {
allowedTagsEmpty: empty("category.allowed_tags"),
allowedTagGroupsEmpty: empty("category.allowed_tag_groups"),
disableAllowGlobalTags: and("allowedTagsEmpty", "allowedTagGroupsEmpty"),
+
+ @action
+ onTagGroupChange(rtg, valueArray) {
+ // A little strange, but we're using a multi-select component
+ // to select a single tag group. This action takes the array
+ // and extracts the first value in it.
+ set(rtg, "name", valueArray[0]);
+ },
+
+ @action
+ addRequiredTagGroup() {
+ this.category.required_tag_groups.pushObject({
+ min_count: 1,
+ });
+ },
+
+ @action
+ deleteRequiredTagGroup(rtg) {
+ this.category.required_tag_groups.removeObject(rtg);
+ },
});
diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
index f3d984bcd5..4489a1353b 100644
--- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
+++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
@@ -14,9 +14,11 @@ import I18n from "I18n";
import { action } from "@ember/object";
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
-import { MOMENT_MONDAY, now, startOfDay } from "discourse/lib/time-utils";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
-import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
+import {
+ TIME_SHORTCUT_TYPES,
+ timeShortcuts,
+} from "discourse/lib/time-shortcut";
import ItsATrap from "@discourse/itsatrap";
export default Component.extend({
@@ -81,23 +83,19 @@ export default Component.extend({
},
@discourseComputed()
- customTimeShortcutOptions() {
+ timeOptions() {
const timezone = this.currentUser.resolvedTimezone(this.currentUser);
+ const shortcuts = timeShortcuts(timezone);
+
return [
- {
- icon: "far-clock",
- id: "two_weeks",
- label: "time_shortcut.two_weeks",
- time: startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY)),
- timeFormatKey: "dates.long_no_year",
- },
- {
- icon: "far-calendar-plus",
- id: "six_months",
- label: "time_shortcut.six_months",
- time: startOfDay(now(timezone).add(6, "months").startOf("month")),
- timeFormatKey: "dates.long_no_year",
- },
+ shortcuts.laterToday(),
+ shortcuts.tomorrow(),
+ shortcuts.laterThisWeek(),
+ shortcuts.thisWeekend(),
+ shortcuts.monday(),
+ shortcuts.twoWeeks(),
+ shortcuts.nextMonth(),
+ shortcuts.sixMonths(),
];
},
diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js
index 9d577d8b83..d54f52160e 100644
--- a/app/assets/javascripts/discourse/app/components/emoji-picker.js
+++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js
@@ -153,9 +153,10 @@ export default Component.extend({
},
@action
- onClose() {
+ onClose(event) {
+ event?.stopPropagation();
document.removeEventListener("click", this.handleOutsideClick);
- this.onEmojiPickerClose && this.onEmojiPickerClose();
+ this.onEmojiPickerClose && this.onEmojiPickerClose(event);
},
diversityScales: computed("selectedDiversity", function () {
@@ -221,7 +222,7 @@ export default Component.extend({
});
if (this.site.isMobileDevice) {
- this.onClose();
+ this.onClose(event);
}
},
@@ -236,7 +237,7 @@ export default Component.extend({
@action
keydown(event) {
if (event.code === "Escape") {
- this.onClose();
+ this.onClose(event);
return false;
}
},
@@ -334,7 +335,7 @@ export default Component.extend({
handleOutsideClick(event) {
const emojiPicker = document.querySelector(".emoji-picker");
if (emojiPicker && !emojiPicker.contains(event.target)) {
- this.onClose();
+ this.onClose(event);
}
},
});
diff --git a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js
index 3c7e17d634..3999c2b756 100644
--- a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js
+++ b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js
@@ -1,5 +1,4 @@
import Component from "@ember/component";
-import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -37,26 +36,7 @@ export default Component.extend({
return group
.save(opts)
- .then((data) => {
- if (data.user_count) {
- const controller = showModal("group-default-notifications", {
- model: {
- count: data.user_count,
- },
- });
-
- controller.set("onClose", () => {
- this.updateExistingUsers = controller.updateExistingUsers;
- this.send("save");
- });
-
- return;
- }
-
- if (data.route_to) {
- DiscourseURL.routeTo(data.route_to);
- }
-
+ .then(() => {
this.setProperties({
saved: true,
updateExistingUsers: null,
@@ -66,7 +46,21 @@ export default Component.extend({
this.afterSave();
}
})
- .catch(popupAjaxError)
+ .catch((error) => {
+ const json = error.jqXHR.responseJSON;
+ if (error.jqXHR.status === 422 && json.user_count) {
+ const controller = showModal("group-default-notifications", {
+ model: { count: json.user_count },
+ });
+
+ controller.set("onClose", () => {
+ this.updateExistingUsers = controller.updateExistingUsers;
+ this.send("save");
+ });
+ } else {
+ popupAjaxError(error);
+ }
+ })
.finally(() => this.set("saving", false));
},
},
diff --git a/app/assets/javascripts/discourse/app/components/json-editor.js b/app/assets/javascripts/discourse/app/components/json-editor.js
index c0f418d8bb..2ac1dcc734 100644
--- a/app/assets/javascripts/discourse/app/components/json-editor.js
+++ b/app/assets/javascripts/discourse/app/components/json-editor.js
@@ -29,8 +29,9 @@ export default Component.extend({
schema: this.model.jsonSchema,
disable_array_delete_all_rows: true,
disable_array_delete_last_row: true,
- disable_array_reorder: true,
- disable_array_copy: true,
+ disable_array_reorder: false,
+ disable_array_copy: false,
+ enable_array_copy: true,
disable_edit_json: true,
disable_properties: true,
disable_collapse: true,
@@ -70,8 +71,11 @@ export default Component.extend({
class DiscourseJsonSchemaEditorIconlib {
constructor() {
this.mapping = {
- delete: "times",
+ delete: "trash-alt",
add: "plus",
+ moveup: "arrow-up",
+ movedown: "arrow-down",
+ copy: "copy",
};
}
diff --git a/app/assets/javascripts/discourse/app/components/pick-files-button.js b/app/assets/javascripts/discourse/app/components/pick-files-button.js
index 9ab6465bcd..3f2482ad50 100644
--- a/app/assets/javascripts/discourse/app/components/pick-files-button.js
+++ b/app/assets/javascripts/discourse/app/components/pick-files-button.js
@@ -1,25 +1,47 @@
import Component from "@ember/component";
-import { action } from "@ember/object";
-import { empty } from "@ember/object/computed";
-import computed, { bind } from "discourse-common/utils/decorators";
-import I18n from "I18n";
import bootbox from "bootbox";
+import { isBlank } from "@ember/utils";
+import {
+ authorizedExtensions,
+ authorizesAllExtensions,
+} from "discourse/lib/uploads";
+import { action } from "@ember/object";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
+import I18n from "I18n";
+// This picker is intended to be used with UppyUploadMixin or with
+// ComposerUploadUppy, which is why there are no change events registered
+// for the input. They are handled by the uppy mixins directly.
+//
+// However, if you provide an onFilesPicked action to this component, the change
+// binding will still be added, and the file type will be validated here. This
+// is sometimes useful if you need to do something outside the uppy upload with
+// the file, such as directly using JSON or CSV data from a file in JS.
export default Component.extend({
+ fileInputId: null,
+ fileInputClass: null,
+ fileInputDisabled: false,
classNames: ["pick-files-button"],
- acceptedFileTypes: null,
- acceptAnyFile: empty("acceptedFileTypes"),
+ acceptedFormatsOverride: null,
+ allowMultiple: false,
+ showButton: false,
didInsertElement() {
this._super(...arguments);
- const fileInput = this.element.querySelector("input");
- this.set("fileInput", fileInput);
- fileInput.addEventListener("change", this.onChange, false);
+
+ if (this.onFilesPicked) {
+ const fileInput = this.element.querySelector("input");
+ this.set("fileInput", fileInput);
+ fileInput.addEventListener("change", this.onChange, false);
+ }
},
willDestroyElement() {
this._super(...arguments);
- this.fileInput.removeEventListener("change", this.onChange);
+
+ if (this.onFilesPicked) {
+ this.fileInput.removeEventListener("change", this.onChange);
+ }
},
@bind
@@ -28,33 +50,27 @@ export default Component.extend({
this._filesPicked(files);
},
- @computed
- acceptedFileTypesString() {
- if (!this.acceptedFileTypes) {
- return null;
- }
-
- return this.acceptedFileTypes.join(",");
+ @discourseComputed()
+ acceptsAllFormats() {
+ return (
+ this.capabilities.isIOS ||
+ authorizesAllExtensions(this.currentUser.staff, this.siteSettings)
+ );
},
- @computed
- acceptedExtensions() {
- if (!this.acceptedFileTypes) {
- return null;
+ @discourseComputed()
+ acceptedFormats() {
+ // the acceptedFormatsOverride can be a list of extensions or mime types
+ if (!isBlank(this.acceptedFormatsOverride)) {
+ return this.acceptedFormatsOverride;
}
- return this.acceptedFileTypes
- .filter((type) => type.startsWith("."))
- .map((type) => type.substring(1));
- },
+ const extensions = authorizedExtensions(
+ this.currentUser.staff,
+ this.siteSettings
+ );
- @computed
- acceptedMimeTypes() {
- if (!this.acceptedFileTypes) {
- return null;
- }
-
- return this.acceptedFileTypes.filter((type) => !type.startsWith("."));
+ return extensions.map((ext) => `.${ext}`).join();
},
@action
@@ -79,25 +95,18 @@ export default Component.extend({
_haveAcceptedTypes(files) {
for (const file of files) {
- if (
- !(this._hasAcceptedExtension(file) || this._hasAcceptedMimeType(file))
- ) {
+ if (!this._hasAcceptedExtensionOrType(file)) {
return false;
}
}
return true;
},
- _hasAcceptedExtension(file) {
+ _hasAcceptedExtensionOrType(file) {
const extension = this._fileExtension(file.name);
return (
- !this.acceptedExtensions || this.acceptedExtensions.includes(extension)
- );
- },
-
- _hasAcceptedMimeType(file) {
- return (
- !this.acceptedMimeTypes || this.acceptedMimeTypes.includes(file.type)
+ this.acceptedFormats.includes(`.${extension}`) ||
+ this.acceptedFormats.includes(file.type)
);
},
diff --git a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js
index 261d01bc08..c6c088decb 100644
--- a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js
+++ b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js
@@ -58,24 +58,6 @@ export default MountWidget.extend({
);
},
- beforePatch() {
- this.prevHeight = document.body.clientHeight;
- this.prevScrollTop = document.body.scrollTop;
- },
-
- afterPatch() {
- const height = document.body.clientHeight;
-
- // This hack is for when swapping out many cloaked views at once
- // when using keyboard navigation. It could suddenly move the scroll
- if (
- this.prevHeight === height &&
- document.body.scrollTop !== this.prevScrollTop
- ) {
- document.body.scroll({ left: 0, top: this.prevScrollTop });
- }
- },
-
scrolled() {
if (this.isDestroyed || this.isDestroying) {
return;
@@ -103,7 +85,7 @@ export default MountWidget.extend({
const slack = Math.round(windowHeight * 5);
const onscreen = [];
const nearby = [];
- const windowTop = document.documentElement.scrollTop;
+ const windowTop = document.scrollingElement.scrollTop;
const postsWrapperTop = domUtils.offset(
document.querySelector(".posts-wrapper")
).top;
@@ -202,9 +184,7 @@ export default MountWidget.extend({
const elem = postsNodes.item(onscreen[0]);
const elemId = elem.id;
const elemPos = domUtils.position(elem);
- const distToElement = elemPos
- ? document.body.scrollTop - elemPos.top
- : 0;
+ const distToElement = elemPos?.top || 0;
const topRefresh = () => {
refresh(() => {
@@ -213,7 +193,7 @@ export default MountWidget.extend({
// Quickly going back might mean the element is destroyed
const position = domUtils.position(refreshedElem);
if (position && position.top) {
- let whereY = position.top + distToElement;
+ let whereY = position.top - distToElement;
document.documentElement.scroll({ top: whereY, left: 0 });
// This seems weird, but somewhat infrequently a rerender
diff --git a/app/assets/javascripts/discourse/app/components/search-result-entry.js b/app/assets/javascripts/discourse/app/components/search-result-entry.js
index 73e340671f..9312174ed1 100644
--- a/app/assets/javascripts/discourse/app/components/search-result-entry.js
+++ b/app/assets/javascripts/discourse/app/components/search-result-entry.js
@@ -1,4 +1,6 @@
import Component from "@ember/component";
+import { action } from "@ember/object";
+import { logSearchLinkClick } from "discourse/lib/search";
export default Component.extend({
tagName: "div",
@@ -6,4 +8,15 @@ export default Component.extend({
classNameBindings: ["bulkSelectEnabled"],
attributeBindings: ["role"],
role: "listitem",
+
+ @action
+ logClick(topicId) {
+ if (this.searchLogId && topicId) {
+ logSearchLinkClick({
+ searchLogId: this.searchLogId,
+ searchResultId: topicId,
+ searchResultType: "topic",
+ });
+ }
+ },
});
diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js
index d541ad74da..82a4d9f843 100644
--- a/app/assets/javascripts/discourse/app/components/site-header.js
+++ b/app/assets/javascripts/discourse/app/components/site-header.js
@@ -444,6 +444,5 @@ export default SiteHeaderComponent.extend({
export function headerTop() {
const header = document.querySelector("header.d-header");
- const headerOffsetTop = header.offsetTop ? header.offsetTop : 0;
- return headerOffsetTop - document.body.scrollTop;
+ return header.offsetTop ? header.offsetTop : 0;
}
diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
index 5839191f48..46890b6afe 100644
--- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
+++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
@@ -9,7 +9,7 @@ import {
} from "discourse/lib/time-utils";
import {
TIME_SHORTCUT_TYPES,
- defaultShortcutOptions,
+ defaultTimeShortcuts,
specialShortcutOptions,
} from "discourse/lib/time-shortcut";
import discourseComputed, {
@@ -169,30 +169,23 @@ export default Component.extend({
},
@discourseComputed(
+ "timeShortcuts",
"hiddenOptions",
- "customOptions",
"customLabels",
"userTimezone"
)
- options(hiddenOptions, customOptions, customLabels, userTimezone) {
+ options(timeShortcuts, hiddenOptions, customLabels, userTimezone) {
this._loadLastUsedCustomDatetime();
- let options = defaultShortcutOptions(userTimezone);
+ let options;
+ if (timeShortcuts && timeShortcuts.length) {
+ options = timeShortcuts;
+ } else {
+ options = defaultTimeShortcuts(userTimezone);
+ }
this._hideDynamicOptions(options);
- options = options.concat(customOptions);
-
- options.sort((a, b) => {
- if (a.time < b.time) {
- return -1;
- }
- if (a.time > b.time) {
- return 1;
- }
- return 0;
- });
let specialOptions = specialShortcutOptions();
-
if (this.lastCustomDate && this.lastCustomTime) {
let lastCustom = specialOptions.findBy(
"id",
@@ -202,7 +195,6 @@ export default Component.extend({
lastCustom.timeFormatKey = "dates.long_no_year";
lastCustom.hidden = false;
}
-
options = options.concat(specialOptions);
if (hiddenOptions.length > 0) {
diff --git a/app/assets/javascripts/discourse/app/components/topic-entrance.js b/app/assets/javascripts/discourse/app/components/topic-entrance.js
index 2b73f4de6e..6cfe3d4fb6 100644
--- a/app/assets/javascripts/discourse/app/components/topic-entrance.js
+++ b/app/assets/javascripts/discourse/app/components/topic-entrance.js
@@ -2,7 +2,7 @@ import CleansUp from "discourse/mixins/cleans-up";
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
-import discourseComputed from "discourse-common/utils/decorators";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
import { scheduleOnce } from "@ember/runloop";
function entranceDate(dt, showTime) {
@@ -31,9 +31,11 @@ function entranceDate(dt, showTime) {
export default Component.extend(CleansUp, {
elementId: "topic-entrance",
classNameBindings: ["visible::hidden"],
- _position: null,
topic: null,
visible: null,
+ _position: null,
+ _originalActiveElement: null,
+ _activeButton: null,
@discourseComputed("topic.created_at")
createdDate: (createdAt) => new Date(createdAt),
@@ -74,12 +76,64 @@ export default Component.extend(CleansUp, {
$self.css(pos);
},
+ @bind
+ _escListener(e) {
+ if (e.key === "Escape") {
+ this.cleanUp();
+ } else if (e.key === "Tab") {
+ if (this._activeButton === "top") {
+ this._jumpBottomButton().focus();
+ this._activeButton = "bottom";
+ e.preventDefault();
+ } else if (this._activeButton === "bottom") {
+ this._jumpTopButton().focus();
+ this._activeButton = "top";
+ e.preventDefault();
+ }
+ }
+ },
+
+ _jumpTopButton() {
+ return this.element.querySelector(".jump-top");
+ },
+
+ _jumpBottomButton() {
+ return this.element.querySelector(".jump-bottom");
+ },
+
+ _setupEscListener() {
+ document.body.addEventListener("keydown", this._escListener);
+ },
+
+ _removeEscListener() {
+ document.body.removeEventListener("keydown", this._escListener);
+ },
+
+ _trapFocus() {
+ this._originalActiveElement = document.activeElement;
+ this._jumpTopButton().focus();
+ this._activeButton = "top";
+ },
+
+ _releaseFocus() {
+ if (this._originalActiveElement) {
+ this._originalActiveElement.focus();
+ this._originalActiveElement = null;
+ }
+ },
+
+ _applyDomChanges() {
+ this._setCSS();
+ this._setupEscListener();
+ this._trapFocus();
+ },
+
_show(data) {
this._position = data.position;
this.setProperties({ topic: data.topic, visible: true });
- scheduleOnce("afterRender", this, this._setCSS);
+ scheduleOnce("afterRender", this, this._applyDomChanges);
$("html")
.off("mousedown.topic-entrance")
@@ -98,6 +152,8 @@ export default Component.extend(CleansUp, {
cleanUp() {
this.setProperties({ topic: null, visible: false });
$("html").off("mousedown.topic-entrance");
+ this._removeEscListener();
+ this._releaseFocus();
},
willDestroyElement() {
diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js
index 7e3a46dec8..4bdbd7de4b 100644
--- a/app/assets/javascripts/discourse/app/components/topic-list-item.js
+++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js
@@ -1,4 +1,7 @@
-import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import discourseComputed, {
+ bind,
+ observes,
+} from "discourse-common/utils/decorators";
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
@@ -58,6 +61,13 @@ export default Component.extend({
if (this.selected && this.selected.includes(this.topic)) {
this.element.querySelector("input.bulk-select").checked = true;
}
+ if (this._shouldFocusLastVisited()) {
+ const title = this._titleElement();
+ if (title) {
+ title.addEventListener("focus", this._onTitleFocus);
+ title.addEventListener("blur", this._onTitleBlur);
+ }
+ }
});
}
},
@@ -98,6 +108,13 @@ export default Component.extend({
if (this.includeUnreadIndicator) {
this.messageBus.unsubscribe(this.unreadIndicatorChannel);
}
+ if (this._shouldFocusLastVisited()) {
+ const title = this._titleElement();
+ if (title) {
+ title.removeEventListener("focus", this._onTitleFocus);
+ title.removeEventListener("blur", this._onTitleBlur);
+ }
+ }
},
@discourseComputed("topic.id")
@@ -259,12 +276,17 @@ export default Component.extend({
return;
}
- const $topic = $(this.element);
- $topic
- .addClass("highlighted")
- .attr("data-islastviewedtopic", opts.isLastViewedTopic);
-
- $topic.on("animationend", () => $topic.removeClass("highlighted"));
+ this.element.classList.add("highlighted");
+ this.element.setAttribute(
+ "data-islastviewedtopic",
+ opts.isLastViewedTopic
+ );
+ this.element.addEventListener("animationend", () => {
+ this.element.classList.remove("highlighted");
+ });
+ if (opts.isLastViewedTopic && this._shouldFocusLastVisited()) {
+ this._titleElement()?.focus();
+ }
});
},
@@ -279,4 +301,30 @@ export default Component.extend({
this.highlight();
}
}),
+
+ @bind
+ _onTitleFocus() {
+ if (this.element && !this.isDestroying && !this.isDestroyed) {
+ this._mainLinkElement().classList.add("focused");
+ }
+ },
+
+ @bind
+ _onTitleBlur() {
+ if (this.element && !this.isDestroying && !this.isDestroyed) {
+ this._mainLinkElement().classList.remove("focused");
+ }
+ },
+
+ _shouldFocusLastVisited() {
+ return !this.site.mobileView && this.focusLastVisitedTopic;
+ },
+
+ _mainLinkElement() {
+ return this.element.querySelector(".main-link");
+ },
+
+ _titleElement() {
+ return this.element.querySelector(".main-link .title");
+ },
});
diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js
index 9c2b2ef70a..a08444022c 100644
--- a/app/assets/javascripts/discourse/app/components/topic-navigation.js
+++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js
@@ -45,6 +45,11 @@ export default Component.extend(PanEvents, {
let info = this.info;
+ // Safari's window.innerWidth doesn't match CSS media queries
+ let windowWidth = this.capabilities.isSafari
+ ? document.documentElement.clientWidth
+ : window.innerWidth;
+
if (info.get("topicProgressExpanded")) {
info.set("renderTimeline", true);
} else {
@@ -55,7 +60,7 @@ export default Component.extend(PanEvents, {
if (composer) {
renderTimeline =
- window.innerWidth > MIN_WIDTH_TIMELINE &&
+ windowWidth > MIN_WIDTH_TIMELINE &&
window.innerHeight - composer.offsetHeight - headerOffset() >
MIN_HEIGHT_TIMELINE;
}
diff --git a/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js b/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js
index 1b794cd5c4..02fd2b93fa 100644
--- a/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js
+++ b/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js
@@ -50,6 +50,12 @@ export default Component.extend(UppyUploadMixin, {
return { imagesOnly: true };
},
+ _uppyReady() {
+ this._onPreProcessComplete(() => {
+ this.set("processing", false);
+ });
+ },
+
uploadDone(upload) {
this.setProperties({
imageFilesize: upload.human_filesize,
diff --git a/app/assets/javascripts/discourse/app/components/user-info.js b/app/assets/javascripts/discourse/app/components/user-info.js
index 024825ed78..dc9cf2e86d 100644
--- a/app/assets/javascripts/discourse/app/components/user-info.js
+++ b/app/assets/javascripts/discourse/app/components/user-info.js
@@ -13,6 +13,8 @@ export default Component.extend({
attributeBindings: ["data-username"],
size: "small",
"data-username": alias("user.username"),
+ includeLink: true,
+ includeAvatar: true,
@discourseComputed("user.username")
userPath(username) {
diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js
index 6fce0c15ea..0d906bbf90 100644
--- a/app/assets/javascripts/discourse/app/controllers/bookmark.js
+++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js
@@ -1,4 +1,5 @@
import Controller from "@ember/controller";
+import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { Promise } from "rsvp";
@@ -10,28 +11,53 @@ export function openBookmarkModal(
onCloseWithoutSaving: null,
onAfterSave: null,
onAfterDelete: null,
+ },
+ options = {
+ use_polymorphic_bookmarks: false,
}
) {
return new Promise((resolve) => {
const modalTitle = () => {
- if (bookmark.for_topic) {
- return bookmark.id
- ? "post.bookmarks.edit_for_topic"
- : "post.bookmarks.create_for_topic";
+ if (options.use_polymorphic_bookmarks) {
+ return I18n.t(
+ bookmark.id ? "bookmarks.edit_for" : "bookmarks.create_for",
+ {
+ type: bookmark.bookmarkable_type,
+ }
+ );
+ } else if (bookmark.for_topic) {
+ return I18n.t(
+ bookmark.id
+ ? "post.bookmarks.edit_for_topic"
+ : "post.bookmarks.create_for_topic"
+ );
+ } else {
+ return I18n.t(
+ bookmark.id ? "post.bookmarks.edit" : "post.bookmarks.create"
+ );
}
- return bookmark.id ? "post.bookmarks.edit" : "post.bookmarks.create";
};
+
+ const model = {
+ id: bookmark.id,
+ reminderAt: bookmark.reminder_at,
+ autoDeletePreference: bookmark.auto_delete_preference,
+ name: bookmark.name,
+ };
+
+ if (options.use_polymorphic_bookmarks) {
+ model.bookmarkableId = bookmark.bookmarkable_id;
+ model.bookmarkableType = bookmark.bookmarkable_type;
+ } else {
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ model.postId = bookmark.post_id;
+ model.topicId = bookmark.topic_id;
+ model.forTopic = bookmark.for_topic;
+ }
+
let modalController = showModal("bookmark", {
- model: {
- postId: bookmark.post_id,
- topicId: bookmark.topic_id,
- id: bookmark.id,
- reminderAt: bookmark.reminder_at,
- autoDeletePreference: bookmark.auto_delete_preference,
- name: bookmark.name,
- forTopic: bookmark.for_topic,
- },
- title: modalTitle(),
+ model,
+ titleTranslated: modalTitle(),
modalClass: "bookmark-with-reminder",
});
modalController.setProperties({
diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js
index 3af5345297..c57d70fa75 100644
--- a/app/assets/javascripts/discourse/app/controllers/composer.js
+++ b/app/assets/javascripts/discourse/app/controllers/composer.js
@@ -1331,6 +1331,7 @@ export default Controller.extend({
this.close();
})
.finally(() => {
+ this.appEvents.trigger("composer:cancelled");
resolve();
});
},
@@ -1338,6 +1339,7 @@ export default Controller.extend({
this._saveDraft();
this.model.clearState();
this.close();
+ this.appEvents.trigger("composer:cancelled");
resolve();
},
// needed to resume saving drafts if composer stays open
@@ -1351,6 +1353,7 @@ export default Controller.extend({
this.close();
})
.finally(() => {
+ this.appEvents.trigger("composer:cancelled");
resolve();
});
}
@@ -1432,17 +1435,12 @@ export default Controller.extend({
tagValidation(category, tags, lastValidatedAt) {
const tagsArray = tags || [];
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)
- ) {
+ // category.minimumRequiredTags incorporates both minimum_required_tags, and required_tag_groups
+ if (category.minimumRequiredTags > 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,
+ count: category.minimumRequiredTags,
}),
lastShownAt: lastValidatedAt,
});
diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js
index c364cb3f81..65922b664a 100644
--- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js
+++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js
@@ -3,6 +3,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
import {
getSearchKey,
isValidSearchTerm,
+ logSearchLinkClick,
searchContextDescription,
translateResults,
updateRecentSearches,
@@ -470,15 +471,10 @@ export default Controller.extend({
logClick(topicId) {
if (this.get("model.grouped_search_result.search_log_id") && topicId) {
- ajax("/search/click", {
- type: "POST",
- data: {
- search_log_id: this.get(
- "model.grouped_search_result.search_log_id"
- ),
- search_result_id: topicId,
- search_result_type: "topic",
- },
+ logSearchLinkClick({
+ searchLogId: this.get("model.grouped_search_result.search_log_id"),
+ searchResultId: topicId,
+ searchResultType: "topic",
});
}
},
diff --git a/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js b/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js
deleted file mode 100644
index 62c000b473..0000000000
--- a/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Controller from "@ember/controller";
-import { action } from "@ember/object";
-import discourseComputed from "discourse-common/utils/decorators";
-import { getWebauthnCredential } from "discourse/lib/webauthn";
-import ModalFunctionality from "discourse/mixins/modal-functionality";
-import { SECOND_FACTOR_METHODS } from "discourse/models/user";
-import I18n from "I18n";
-import bootbox from "bootbox";
-
-export default Controller.extend(ModalFunctionality, {
- showSecondFactor: false,
- secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
- secondFactorToken: null,
- securityKeyCredential: null,
-
- inProgress: false,
-
- onShow() {
- this.setProperties({
- showSecondFactor: false,
- secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
- secondFactorToken: null,
- securityKeyCredential: null,
- });
- },
-
- @discourseComputed("inProgress", "securityKeyCredential", "secondFactorToken")
- disabled(inProgress, securityKeyCredential, secondFactorToken) {
- return inProgress || (!securityKeyCredential && !secondFactorToken);
- },
-
- setResult(result) {
- this.setProperties({
- otherMethodAllowed: result.multiple_second_factor_methods,
- secondFactorRequired: true,
- showLoginButtons: false,
- backupEnabled: result.backup_enabled,
- showSecondFactor: result.totp_enabled,
- showSecurityKey: result.security_key_enabled,
- secondFactorMethod: result.security_key_enabled
- ? SECOND_FACTOR_METHODS.SECURITY_KEY
- : SECOND_FACTOR_METHODS.TOTP,
- securityKeyChallenge: result.challenge,
- securityKeyAllowedCredentialIds: result.allowed_credential_ids,
- });
- },
-
- @action
- authenticateSecurityKey() {
- getWebauthnCredential(
- this.securityKeyChallenge,
- this.securityKeyAllowedCredentialIds,
- (credentialData) => {
- this.set("securityKeyCredential", credentialData);
- this.send("authenticate");
- },
- (errorMessage) => {
- this.flash(errorMessage, "error");
- }
- );
- },
-
- @action
- authenticate() {
- this.set("inProgress", true);
- this.model
- .grantAdmin({
- second_factor_token:
- this.securityKeyCredential || this.secondFactorToken,
- second_factor_method: this.secondFactorMethod,
- timezone: moment.tz.guess(),
- })
- .then((result) => {
- if (result.success) {
- this.send("closeModal");
- bootbox.alert(I18n.t("admin.user.grant_admin_success"));
- } else {
- this.flash(result.error, "error");
- this.setResult(result);
- }
- })
- .finally(() => this.set("inProgress", false));
- },
-});
diff --git a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js
index 929581be2e..77e77f3e95 100644
--- a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js
+++ b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js
@@ -18,7 +18,7 @@ export default Controller.extend(ModalFunctionality, {
this.set("loading", true);
this.model
.updateNotificationLevel({
- level: "ignored",
+ level: "ignore",
expiringAt: this.ignoredUntil,
})
.then(() => {
diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js
index 4a6307d75c..7079a5ea2c 100644
--- a/app/assets/javascripts/discourse/app/controllers/invites-show.js
+++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js
@@ -1,4 +1,4 @@
-import { alias, notEmpty, or, readOnly } from "@ember/object/computed";
+import { alias, not, or, readOnly } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import DiscourseURL from "discourse/lib/url";
import EmberObject from "@ember/object";
@@ -33,7 +33,7 @@ export default Controller.extend(
emailVerifiedByLink: alias("model.email_verified_by_link"),
differentExternalEmail: alias("model.different_external_email"),
accountUsername: alias("model.username"),
- passwordRequired: notEmpty("accountPassword"),
+ passwordRequired: not("externalAuthsOnly"),
successMessage: null,
errorMessage: null,
userFields: null,
diff --git a/app/assets/javascripts/discourse/app/controllers/review-index.js b/app/assets/javascripts/discourse/app/controllers/review-index.js
index 4c19441594..6c2c9a84b1 100644
--- a/app/assets/javascripts/discourse/app/controllers/review-index.js
+++ b/app/assets/javascripts/discourse/app/controllers/review-index.js
@@ -111,7 +111,7 @@ export default Controller.extend({
if (newList.length === 0) {
this.refreshModel();
} else {
- this.set("reviewables", newList);
+ this.reviewables.setObjects(newList);
}
},
diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js
index 996e5f06ff..4de8d672c3 100644
--- a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js
+++ b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js
@@ -194,7 +194,11 @@ export default Controller.extend({
type: response.callback_method,
data: { second_factor_nonce: this.nonce },
})
- .then(() => DiscourseURL.routeTo(response.redirect_path))
+ .then((callbackResponse) => {
+ const redirectUrl =
+ callbackResponse.redirect_url || response.redirect_url;
+ DiscourseURL.routeTo(redirectUrl);
+ })
.catch((error) => this.displayError(extractError(error)));
})
.catch((error) => {
diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js
index 1398450d05..9e0b113774 100644
--- a/app/assets/javascripts/discourse/app/controllers/topic.js
+++ b/app/assets/javascripts/discourse/app/controllers/topic.js
@@ -754,6 +754,7 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
toggleBookmark(post) {
if (!this.currentUser) {
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
@@ -784,6 +785,39 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
+ toggleBookmarkPolymorphic(post) {
+ if (!this.currentUser) {
+ return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
+ } else if (post) {
+ const bookmarkForPost = this.model.bookmarks.find(
+ (bookmark) =>
+ bookmark.bookmarkable_id === post.id &&
+ bookmark.bookmarkable_type === "Post"
+ );
+ return this._modifyPostBookmark(
+ bookmarkForPost ||
+ Bookmark.create({
+ bookmarkable_id: post.id,
+ bookmarkable_type: "Post",
+ auto_delete_preference: this.currentUser
+ .bookmark_auto_delete_preference,
+ }),
+ post
+ );
+ } else {
+ return this._toggleTopicLevelBookmarkPolymorphic().then(
+ (changedIds) => {
+ if (!changedIds) {
+ return;
+ }
+ changedIds.forEach((id) =>
+ this.appEvents.trigger("post-stream:refresh", { id })
+ );
+ }
+ );
+ }
+ },
+
jumpToIndex(index) {
this._jumpToIndex(index);
},
@@ -1238,46 +1272,56 @@ export default Controller.extend(bufferedProperty("model"), {
},
_modifyTopicBookmark(bookmark) {
- return openBookmarkModal(bookmark, {
- onAfterSave: (savedData) => {
- this._syncBookmarks(savedData);
- this.model.set("bookmarking", false);
- this.model.set("bookmarked", true);
- this.model.incrementProperty("bookmarksWereChanged");
- this.appEvents.trigger(
- "bookmarks:changed",
- savedData,
- bookmark.attachedTo()
- );
+ return openBookmarkModal(
+ bookmark,
+ {
+ onAfterSave: (savedData) => {
+ this._syncBookmarks(savedData);
+ this.model.set("bookmarking", false);
+ this.model.set("bookmarked", true);
+ this.model.incrementProperty("bookmarksWereChanged");
+ this.appEvents.trigger(
+ "bookmarks:changed",
+ savedData,
+ bookmark.attachedTo()
+ );
- // TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed.
- this.appEvents.trigger("topic:bookmark-toggled");
+ // TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed.
+ this.appEvents.trigger("topic:bookmark-toggled");
+ },
+ onAfterDelete: (topicBookmarked, bookmarkId) => {
+ this.model.removeBookmark(bookmarkId);
+ },
},
- onAfterDelete: (topicBookmarked, bookmarkId) => {
- this.model.removeBookmark(bookmarkId);
- },
- });
+ { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks }
+ );
},
_modifyPostBookmark(bookmark, post) {
- return openBookmarkModal(bookmark, {
- onCloseWithoutSaving: () => {
- post.appEvents.trigger("post-stream:refresh", {
- id: bookmark.post_id,
- });
+ return openBookmarkModal(
+ bookmark,
+ {
+ onCloseWithoutSaving: () => {
+ post.appEvents.trigger("post-stream:refresh", {
+ id: this.siteSettings.use_polymorphic_bookmarks
+ ? bookmark.bookmarkable_id
+ : bookmark.post_id,
+ });
+ },
+ onAfterSave: (savedData) => {
+ this._syncBookmarks(savedData);
+ this.model.set("bookmarking", false);
+ post.createBookmark(savedData);
+ this.model.afterPostBookmarked(post, savedData);
+ return [post.id];
+ },
+ onAfterDelete: (topicBookmarked, bookmarkId) => {
+ this.model.removeBookmark(bookmarkId);
+ post.deleteBookmark(topicBookmarked);
+ },
},
- onAfterSave: (savedData) => {
- this._syncBookmarks(savedData);
- this.model.set("bookmarking", false);
- post.createBookmark(savedData);
- this.model.afterPostBookmarked(post, savedData);
- return [post.id];
- },
- onAfterDelete: (topicBookmarked, bookmarkId) => {
- this.model.removeBookmark(bookmarkId);
- post.deleteBookmark(topicBookmarked);
- },
- });
+ { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks }
+ );
},
_syncBookmarks(data) {
@@ -1295,6 +1339,7 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
async _toggleTopicLevelBookmark() {
if (this.model.bookmarking) {
return Promise.resolve();
@@ -1329,6 +1374,41 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
+ async _toggleTopicLevelBookmarkPolymorphic() {
+ if (this.model.bookmarking) {
+ return Promise.resolve();
+ }
+
+ if (this.model.bookmarkCount > 1) {
+ return this._maybeClearAllBookmarks();
+ }
+
+ if (this.model.bookmarkCount === 1) {
+ const topicBookmark = this.model.bookmarks.findBy(
+ "bookmarkable_type",
+ "Topic"
+ );
+ if (topicBookmark) {
+ return this._modifyTopicBookmark(topicBookmark);
+ } else {
+ const bookmark = this.model.bookmarks[0];
+ const post = await this.model.postById(bookmark.bookmarkable_id);
+ return this._modifyPostBookmark(bookmark, post);
+ }
+ }
+
+ if (this.model.bookmarkCount === 0) {
+ return this._modifyTopicBookmark(
+ Bookmark.create({
+ bookmarkable_id: this.model.id,
+ bookmarkable_type: "Topic",
+ auto_delete_preference: this.currentUser
+ .bookmark_auto_delete_preference,
+ })
+ );
+ }
+ },
+
_maybeClearAllBookmarks() {
return new Promise((resolve) => {
bootbox.confirm(
diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js
index 49199aafd7..2efaf9724c 100644
--- a/app/assets/javascripts/discourse/app/controllers/user.js
+++ b/app/assets/javascripts/discourse/app/controllers/user.js
@@ -116,9 +116,9 @@ export default Controller.extend(CanCheckEmails, {
);
},
- @discourseComputed("viewingSelf", "currentUser.staff")
- showNotificationsTab(viewingSelf, staff) {
- return viewingSelf || staff;
+ @discourseComputed("viewingSelf", "currentUser.admin")
+ showNotificationsTab(viewingSelf, isAdmin) {
+ return viewingSelf || isAdmin;
},
@discourseComputed("model.name")
@@ -168,14 +168,19 @@ export default Controller.extend(CanCheckEmails, {
"currentUser.ignored_ids",
"model.ignored",
"model.muted",
- function () {
- if (this.get("model.ignored")) {
- return "changeToIgnored";
- } else if (this.get("model.muted")) {
- return "changeToMuted";
- } else {
- return "changeToNormal";
- }
+ {
+ get() {
+ if (this.get("model.ignored")) {
+ return "changeToIgnored";
+ } else if (this.get("model.muted")) {
+ return "changeToMuted";
+ } else {
+ return "changeToNormal";
+ }
+ },
+ set(key, value) {
+ return value;
+ },
}
),
@@ -249,8 +254,8 @@ export default Controller.extend(CanCheckEmails, {
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
},
- updateNotificationLevel(level) {
- return this.model.updateNotificationLevel({ level });
+ updateNotificationLevel(params) {
+ return this.model.updateNotificationLevel(params);
},
},
});
diff --git a/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js
new file mode 100644
index 0000000000..448cf41a44
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js
@@ -0,0 +1,60 @@
+import { withPluginApi } from "discourse/lib/plugin-api";
+
+// Browsers automatically calculate an aspect ratio based on the width/height attributes of an `
{
+ element.querySelectorAll("img").forEach((img) => {
+ const declaredHeight = parseFloat(img.getAttribute("height"));
+ const declaredWidth = parseFloat(img.getAttribute("width"));
+
+ if (
+ isNaN(declaredHeight) ||
+ isNaN(declaredWidth) ||
+ img.style.aspectRatio
+ ) {
+ return;
+ }
+
+ if (supportsAspectRatio) {
+ img.style.setProperty(
+ "aspect-ratio",
+ `${declaredWidth} / ${declaredHeight}`
+ );
+ } else {
+ // For older browsers (e.g. iOS < 15), we need to apply the aspect ratio manually.
+ // It's not perfect, because it won't recompute on browser resize.
+ // This property is consumed in `topic-post.scss` for responsive images only.
+ // It's a no-op for non-responsive images.
+ const calculatedHeight =
+ img.width / (declaredWidth / declaredHeight);
+
+ img.style.setProperty(
+ "--calculated-height",
+ `${calculatedHeight}px`
+ );
+ }
+ });
+ },
+ { id: "image-aspect-ratio" }
+ );
+ },
+
+ initialize() {
+ withPluginApi("1.2.0", this.initWithApi);
+ },
+};
diff --git a/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js b/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js
index 07d3f02db8..0ad6bc5c33 100644
--- a/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js
+++ b/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js
@@ -6,7 +6,7 @@ export default {
if (queryStrings.indexOf("user_api_public_key") !== -1) {
let params = queryStrings.startsWith("?")
- ? queryStrings.substr(1).split("&")
+ ? queryStrings.slice(1).split("&")
: [];
params = params.filter((param) => {
diff --git a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
index b8592c13eb..b1f330c5c7 100644
--- a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
+++ b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
@@ -11,7 +11,8 @@ const DEFER_PRIORITY = 500;
export default {
name: "topic-footer-buttons",
- initialize() {
+ initialize(container) {
+ const siteSettings = container.lookup("site-settings:main");
registerTopicFooterButton({
id: "share-and-invite",
icon: "d-topic-share",
@@ -98,9 +99,14 @@ export default {
if (this.topic.bookmarkCount === 0) {
return I18n.t("bookmarked.help.bookmark");
} else if (this.topic.bookmarkCount === 1) {
- if (
- this.topic.bookmarks.filter((bookmark) => bookmark.for_topic).length
- ) {
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ const anyTopicBookmarks = this.topic.bookmarks.some((bookmark) => {
+ return siteSettings.use_polymorphic_bookmarks
+ ? bookmark.for_topic
+ : bookmark.bookmarkable_type === "Topic";
+ });
+
+ if (anyTopicBookmarks) {
return I18n.t("bookmarked.help.edit_bookmark_for_topic");
} else {
return I18n.t("bookmarked.help.edit_bookmark");
@@ -113,7 +119,10 @@ export default {
return I18n.t("bookmarked.help.unbookmark");
}
},
- action: "toggleBookmark",
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ action: siteSettings.use_polymorphic_bookmarks
+ ? "toggleBookmarkPolymorphic"
+ : "toggleBookmark",
dropdown() {
return this.site.mobileView;
},
diff --git a/app/assets/javascripts/discourse/app/lib/ajax.js b/app/assets/javascripts/discourse/app/lib/ajax.js
index d3591a5ef9..4ec61986c9 100644
--- a/app/assets/javascripts/discourse/app/lib/ajax.js
+++ b/app/assets/javascripts/discourse/app/lib/ajax.js
@@ -29,14 +29,9 @@ export function handleLogoff(xhr) {
}
}
-function handleRedirect(data) {
- if (
- data &&
- data.getResponseHeader &&
- data.getResponseHeader("Discourse-Xhr-Redirect")
- ) {
- window.location.replace(data.responseText);
- window.location.reload();
+function handleRedirect(xhr) {
+ if (xhr && xhr.getResponseHeader("Discourse-Xhr-Redirect")) {
+ window.location = xhr.responseText;
}
}
@@ -99,7 +94,7 @@ export function ajax() {
}
args.success = (data, textStatus, xhr) => {
- handleRedirect(data);
+ handleRedirect(xhr);
handleLogoff(xhr);
run(() => {
diff --git a/app/assets/javascripts/discourse/app/lib/formatter.js b/app/assets/javascripts/discourse/app/lib/formatter.js
index 3a57516b18..b8d8e7509f 100644
--- a/app/assets/javascripts/discourse/app/lib/formatter.js
+++ b/app/assets/javascripts/discourse/app/lib/formatter.js
@@ -25,7 +25,7 @@ export function tinyDateYear(date) {
// TODO: locale support ?
export function toTitleCase(str) {
return str.replace(/\w\S*/g, function (txt) {
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
});
}
diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
index ab11910b45..0e986f53ca 100644
--- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
+++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
@@ -772,7 +772,7 @@ export default {
},
_onScrollEndsCallback() {
- document.querySelector(".topic-post.selected a.tabLoc")?.focus();
+ document.querySelector(".topic-post.selected span.tabLoc")?.focus();
},
categoriesTopicsList() {
diff --git a/app/assets/javascripts/discourse/app/lib/link-hashtags.js b/app/assets/javascripts/discourse/app/lib/link-hashtags.js
index da3391d5d4..9d4201b3c1 100644
--- a/app/assets/javascripts/discourse/app/lib/link-hashtags.js
+++ b/app/assets/javascripts/discourse/app/lib/link-hashtags.js
@@ -22,13 +22,13 @@ export function linkSeenHashtags(elem) {
if (hashtags.length === 0) {
return [];
}
- const slugs = [...hashtags.map((hashtag) => hashtag.innerText.substr(1))];
+ const slugs = [...hashtags.map((hashtag) => hashtag.innerText.slice(1))];
hashtags.forEach((hashtag, index) => {
let slug = slugs[index];
const hasTagSuffix = slug.endsWith(TAG_HASHTAG_POSTFIX);
if (hasTagSuffix) {
- slug = slug.substr(0, slug.length - TAG_HASHTAG_POSTFIX.length);
+ slug = slug.slice(0, slug.length - TAG_HASHTAG_POSTFIX.length);
}
const lowerSlug = slug.toLowerCase();
diff --git a/app/assets/javascripts/discourse/app/lib/link-mentions.js b/app/assets/javascripts/discourse/app/lib/link-mentions.js
index e72575fadd..5d1cf91889 100644
--- a/app/assets/javascripts/discourse/app/lib/link-mentions.js
+++ b/app/assets/javascripts/discourse/app/lib/link-mentions.js
@@ -81,7 +81,7 @@ export function linkSeenMentions(elem, siteSettings) {
...elem.querySelectorAll("span.mention:not(.mention-tested)"),
];
if (mentions.length) {
- const usernames = mentions.map((m) => m.innerText.substr(1));
+ const usernames = mentions.map((m) => m.innerText.slice(1));
updateFound(mentions, usernames);
return usernames
.uniq()
diff --git a/app/assets/javascripts/discourse/app/lib/local-dates.js b/app/assets/javascripts/discourse/app/lib/local-dates.js
new file mode 100644
index 0000000000..65ec52d322
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/local-dates.js
@@ -0,0 +1,11 @@
+export function applyLocalDates(dates, siteSettings) {
+ if (!siteSettings.discourse_local_dates_enabled) {
+ return;
+ }
+
+ const _applyLocalDates = requirejs(
+ "discourse/plugins/discourse-local-dates/initializers/discourse-local-dates"
+ ).applyLocalDates;
+
+ _applyLocalDates(dates, siteSettings);
+}
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index 8375b4f337..74aff2737d 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -84,6 +84,7 @@ import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer
import { registerDesktopNotificationHandler } from "discourse/lib/desktop-notifications";
import { replaceFormatter } from "discourse/lib/utilities";
import { replaceTagRenderer } from "discourse/lib/render-tag";
+import { registerCustomLastUnreadUrlCallback } from "discourse/models/topic";
import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
import { addSearchResultsCallback } from "discourse/lib/search";
import {
@@ -98,7 +99,7 @@ import { consolePrefix } from "discourse/lib/source-identifier";
// based on Semantic Versioning 2.0.0. Please update the changelog at
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
-const PLUGIN_API_VERSION = "1.1.0";
+const PLUGIN_API_VERSION = "1.2.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@@ -1290,6 +1291,21 @@ class PluginApi {
replaceTagRenderer(fn);
}
+ /**
+ * Register a custom last unread url for a topic list item.
+ * If a non-null value is returned, it will be used right away.
+ *
+ * Example:
+ *
+ * function testLastUnreadUrl(context) {
+ * return context.urlForPostNumber(1);
+ * }
+ * api.registerCustomLastUnreadUrlCallback(testLastUnreadUrl);
+ **/
+ registerCustomLastUnreadUrlCallback(fn) {
+ registerCustomLastUnreadUrlCallback(fn);
+ }
+
/**
* Registers custom languages for use with HighlightJS.
*
diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js
index 6df0545ede..40352647ed 100644
--- a/app/assets/javascripts/discourse/app/lib/search.js
+++ b/app/assets/javascripts/discourse/app/lib/search.js
@@ -244,3 +244,14 @@ export function updateRecentSearches(currentUser, term) {
recentSearches.unshiftObject(term);
currentUser.set("recent_searches", recentSearches);
}
+
+export function logSearchLinkClick(params) {
+ ajax("/search/click", {
+ type: "POST",
+ data: {
+ search_log_id: params.searchLogId,
+ search_result_id: params.searchResultId,
+ search_result_type: params.searchResultType,
+ },
+ });
+}
diff --git a/app/assets/javascripts/discourse/app/lib/static-route-builder.js b/app/assets/javascripts/discourse/app/lib/static-route-builder.js
index 200714b4f3..bf5862a5db 100644
--- a/app/assets/javascripts/discourse/app/lib/static-route-builder.js
+++ b/app/assets/javascripts/discourse/app/lib/static-route-builder.js
@@ -25,7 +25,7 @@ export default function (page) {
activate() {
this._super(...arguments);
- jumpToElement(document.location.hash.substr(1));
+ jumpToElement(document.location.hash.slice(1));
},
model() {
diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js
index 635a5c44b2..24d97d6a94 100644
--- a/app/assets/javascripts/discourse/app/lib/text.js
+++ b/app/assets/javascripts/discourse/app/lib/text.js
@@ -142,7 +142,7 @@ export function excerpt(cooked, length) {
if (element.nodeType === Node.TEXT_NODE) {
if (resultLength + element.textContent.length > length) {
- const text = element.textContent.substr(0, length - resultLength);
+ const text = element.textContent.slice(0, length - resultLength);
result += encode(text);
result += "…";
resultLength += text.length;
diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js
index 37d6585cdf..5d59365443 100644
--- a/app/assets/javascripts/discourse/app/lib/time-shortcut.js
+++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js
@@ -6,8 +6,10 @@ import {
nextBusinessWeekStart,
nextMonth,
now,
+ sixMonths,
thisWeekend,
tomorrow,
+ twoWeeks,
} from "discourse/lib/time-utils";
export const TIME_SHORTCUT_TYPES = {
@@ -24,54 +26,15 @@ export const TIME_SHORTCUT_TYPES = {
POST_LOCAL_DATE: "post_local_date",
};
-export function defaultShortcutOptions(timezone) {
+export function defaultTimeShortcuts(timezone) {
+ const shortcuts = timeShortcuts(timezone);
return [
- {
- icon: "angle-right",
- id: TIME_SHORTCUT_TYPES.LATER_TODAY,
- label: "time_shortcut.later_today",
- time: laterToday(timezone),
- timeFormatKey: "dates.time",
- },
- {
- icon: "far-sun",
- id: TIME_SHORTCUT_TYPES.TOMORROW,
- label: "time_shortcut.tomorrow",
- time: tomorrow(timezone),
- timeFormatKey: "dates.time_short_day",
- },
- {
- icon: "angle-double-right",
- id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,
- label: "time_shortcut.later_this_week",
- time: laterThisWeek(timezone),
- timeFormatKey: "dates.time_short_day",
- },
- {
- icon: "bed",
- id: TIME_SHORTCUT_TYPES.THIS_WEEKEND,
- label: "time_shortcut.this_weekend",
- time: thisWeekend(timezone),
- timeFormatKey: "dates.time_short_day",
- },
- {
- icon: "briefcase",
- id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK,
- label:
- now(timezone).day() === MOMENT_MONDAY ||
- now(timezone).day() === MOMENT_SUNDAY
- ? "time_shortcut.start_of_next_business_week_alt"
- : "time_shortcut.start_of_next_business_week",
- time: nextBusinessWeekStart(timezone),
- timeFormatKey: "dates.long_no_year",
- },
- {
- icon: "far-calendar-plus",
- id: TIME_SHORTCUT_TYPES.NEXT_MONTH,
- label: "time_shortcut.next_month",
- time: nextMonth(timezone),
- timeFormatKey: "dates.long_no_year",
- },
+ shortcuts.laterToday(),
+ shortcuts.tomorrow(),
+ shortcuts.laterThisWeek(),
+ shortcuts.thisWeekend(),
+ shortcuts.monday(),
+ shortcuts.nextMonth(),
];
}
@@ -99,3 +62,84 @@ export function specialShortcutOptions() {
},
];
}
+
+export function timeShortcuts(timezone) {
+ return {
+ laterToday() {
+ return {
+ icon: "angle-right",
+ id: TIME_SHORTCUT_TYPES.LATER_TODAY,
+ label: "time_shortcut.later_today",
+ time: laterToday(timezone),
+ timeFormatKey: "dates.time",
+ };
+ },
+ tomorrow() {
+ return {
+ icon: "far-sun",
+ id: TIME_SHORTCUT_TYPES.TOMORROW,
+ label: "time_shortcut.tomorrow",
+ time: tomorrow(timezone),
+ timeFormatKey: "dates.time_short_day",
+ };
+ },
+ laterThisWeek() {
+ return {
+ icon: "angle-double-right",
+ id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,
+ label: "time_shortcut.later_this_week",
+ time: laterThisWeek(timezone),
+ timeFormatKey: "dates.time_short_day",
+ };
+ },
+ thisWeekend() {
+ return {
+ icon: "bed",
+ id: TIME_SHORTCUT_TYPES.THIS_WEEKEND,
+ label: "time_shortcut.this_weekend",
+ time: thisWeekend(timezone),
+ timeFormatKey: "dates.time_short_day",
+ };
+ },
+ monday() {
+ return {
+ icon: "briefcase",
+ id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK,
+ label:
+ now(timezone).day() === MOMENT_MONDAY ||
+ now(timezone).day() === MOMENT_SUNDAY
+ ? "time_shortcut.start_of_next_business_week_alt"
+ : "time_shortcut.start_of_next_business_week",
+ time: nextBusinessWeekStart(timezone),
+ timeFormatKey: "dates.long_no_year",
+ };
+ },
+ nextMonth() {
+ return {
+ icon: "far-calendar-plus",
+ id: TIME_SHORTCUT_TYPES.NEXT_MONTH,
+ label: "time_shortcut.next_month",
+ time: nextMonth(timezone),
+ timeFormatKey: "dates.long_no_year",
+ };
+ },
+ twoWeeks() {
+ return {
+ icon: "far-clock",
+ id: "two_weeks",
+ label: "time_shortcut.two_weeks",
+ time: twoWeeks(timezone),
+ timeFormatKey: "dates.long_no_year",
+ };
+ },
+ sixMonths() {
+ return {
+ icon: "far-calendar-plus",
+ id: "six_months",
+ label: "time_shortcut.six_months",
+ time: sixMonths(timezone),
+ timeFormatKey: "dates.long_no_year",
+ };
+ },
+ };
+}
diff --git a/app/assets/javascripts/discourse/app/lib/time-utils.js b/app/assets/javascripts/discourse/app/lib/time-utils.js
index 5266362e5b..2a8e1e4342 100644
--- a/app/assets/javascripts/discourse/app/lib/time-utils.js
+++ b/app/assets/javascripts/discourse/app/lib/time-utils.js
@@ -43,6 +43,14 @@ export function nextMonth(timezone) {
return startOfDay(now(timezone).add(1, "month").startOf("month"));
}
+export function twoWeeks(timezone) {
+ return startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY));
+}
+
+export function sixMonths(timezone) {
+ return startOfDay(now(timezone).add(6, "months").startOf("month"));
+}
+
export function nextBusinessWeekStart(timezone) {
return startOfDay(now(timezone).add(7, "days")).day(MOMENT_MONDAY);
}
diff --git a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js
index 361ad2e336..08db9414dc 100644
--- a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js
+++ b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js
@@ -33,7 +33,7 @@ const TIMEFRAMES = [
buildTimeframe({
id: "later_this_week",
format: "ddd, h a",
- enabled: (opts) => !opts.canScheduleToday && opts.day > 0 && opts.day < 4,
+ enabled: (opts) => opts.day > 0 && opts.day < 4,
when: (time, timeOfDay) => time.add(2, "day").hour(timeOfDay).minute(0),
}),
buildTimeframe({
diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js
index 37a1e4940f..ff5516da22 100644
--- a/app/assets/javascripts/discourse/app/lib/transform-post.js
+++ b/app/assets/javascripts/discourse/app/lib/transform-post.js
@@ -18,7 +18,7 @@ export function transformBasicPost(post) {
deleted: post.get("deleted"),
deleted_at: post.deleted_at,
user_deleted: post.user_deleted,
- isDeleted: post.deleted_at || post.user_deleted, // xxxxx
+ isDeleted: post.deleted_at || post.user_deleted,
deletedByAvatarTemplate: null,
deletedByUsername: null,
primary_group_name: post.primary_group_name,
@@ -84,6 +84,7 @@ export function transformBasicPost(post) {
readCount: post.readers_count,
canPublishPage: false,
trustLevel: post.trust_level,
+ userSuspended: post.user_suspended,
};
_additionalAttributes.forEach((a) => (postAtts[a] = post[a]));
diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js
index 6b75f07e90..51a640a5d4 100644
--- a/app/assets/javascripts/discourse/app/lib/uploads.js
+++ b/app/assets/javascripts/discourse/app/lib/uploads.js
@@ -10,7 +10,7 @@ function isGUID(value) {
}
export function markdownNameFromFileName(fileName) {
- let name = fileName.substr(0, fileName.lastIndexOf("."));
+ let name = fileName.slice(0, fileName.lastIndexOf("."));
if (isAppleDevice() && isGUID(name)) {
name = I18n.t("upload_selector.default_image_alt_text");
diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js
index 8a78eb0a4b..8cb4ba4552 100644
--- a/app/assets/javascripts/discourse/app/lib/utilities.js
+++ b/app/assets/javascripts/discourse/app/lib/utilities.js
@@ -313,7 +313,7 @@ export function isAppleDevice() {
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
// This will apply hack on all iDevices
let caps = helperContext().capabilities;
- return caps.isIOS && !navigator.userAgent.match(/Trident/g);
+ return caps.isIOS && !window.navigator.userAgent.match(/Trident/g);
}
let iPadDetected = undefined;
@@ -321,8 +321,8 @@ let iPadDetected = undefined;
export function isiPad() {
if (iPadDetected === undefined) {
iPadDetected =
- navigator.userAgent.match(/iPad/g) &&
- !navigator.userAgent.match(/Trident/g);
+ window.navigator.userAgent.match(/iPad/g) &&
+ !window.navigator.userAgent.match(/Trident/g);
}
return iPadDetected;
}
@@ -482,7 +482,7 @@ export function inCodeBlock(text, pos) {
// Character at position `pos` can be in a code block that is unfinished.
// To check this case, we look for any open code blocks after the last closed
// code block.
- const lastOpenBlock = text.substr(end).search(OPEN_CODE_BLOCKS_REGEX);
+ const lastOpenBlock = text.slice(end).search(OPEN_CODE_BLOCKS_REGEX);
return lastOpenBlock !== -1 && pos >= end + lastOpenBlock;
}
@@ -512,8 +512,8 @@ export function translateModKey(string) {
export function clipboardCopy(text) {
// Use the Async Clipboard API when available.
// Requires a secure browsing context (i.e. HTTPS)
- if (navigator.clipboard) {
- return navigator.clipboard.writeText(text).catch(function (err) {
+ if (window.navigator.clipboard) {
+ return window.navigator.clipboard.writeText(text).catch(function (err) {
throw err !== undefined
? err
: new DOMException("The request is not allowed", "NotAllowedError");
@@ -532,12 +532,29 @@ export function clipboardCopy(text) {
//
// Note that the promise passed in should return a Blob with type of
// text/plain.
-export function clipboardCopyAsync(promise) {
+export function clipboardCopyAsync(functionReturningPromise) {
// Use the Async Clipboard API when available.
// Requires a secure browsing context (i.e. HTTPS)
- if (navigator.clipboard) {
- return navigator.clipboard
- .write([new window.ClipboardItem({ "text/plain": promise() })])
+ if (window.navigator.clipboard) {
+ // Firefox does not support window.ClipboardItem yet (it is behind
+ // a flag (dom.events.asyncClipboard.clipboardItem) as at version 87.)
+ // so we need to fall back to the normal non-async clipboard copy, that
+ // works in every browser except Safari.
+ //
+ // TODO: (martin) Look at this on 2022-07-01 to see if support has
+ // changed.
+ if (!window.ClipboardItem) {
+ return functionReturningPromise().then((textBlob) => {
+ return textBlob.text().then((text) => {
+ return clipboardCopy(text);
+ });
+ });
+ }
+
+ return window.navigator.clipboard
+ .write([
+ new window.ClipboardItem({ "text/plain": functionReturningPromise() }),
+ ])
.catch(function (err) {
throw err !== undefined
? err
@@ -546,7 +563,7 @@ export function clipboardCopyAsync(promise) {
}
// ...Otherwise, use document.execCommand() fallback
- return promise().then((textBlob) => {
+ return functionReturningPromise().then((textBlob) => {
textBlob.text().then((text) => {
return clipboardCopyFallback(text);
});
diff --git a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
index de7e63f3dc..cc798fd4b6 100644
--- a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
+++ b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js
@@ -112,6 +112,7 @@ export default Mixin.create({
});
document.addEventListener("mousedown", this._clickOutsideHandler);
+ document.addEventListener("keyup", this._escListener);
_cardClickListenerSelectors.forEach((selector) => {
document
@@ -320,6 +321,7 @@ export default Mixin.create({
this._super(...arguments);
document.removeEventListener("mousedown", this._clickOutsideHandler);
+ document.removeEventListener("keyup", this._escListener);
_cardClickListenerSelectors.forEach((selector) => {
document
@@ -340,14 +342,6 @@ export default Mixin.create({
this._hide();
},
- keyUp(e) {
- if (e.key === "Escape") {
- const target = this.cardTarget;
- this._close();
- target.focus();
- }
- },
-
@bind
_clickOutsideHandler(event) {
if (this.visible) {
@@ -365,4 +359,13 @@ export default Mixin.create({
return true;
},
+
+ @bind
+ _escListener(event) {
+ if (this.visible && event.key === "Escape") {
+ this._close();
+ this.cardTarget?.focus();
+ return;
+ }
+ },
});
diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js
index 0442cf9697..6dce54f500 100644
--- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js
+++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js
@@ -37,6 +37,7 @@ import { run } from "@ember/runloop";
export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
uploadRootPath: "/uploads",
uploadTargetBound: false,
+ useUploadPlaceholders: true,
@bind
_cancelSingleUpload(data) {
@@ -67,9 +68,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
this.editorEl?.removeEventListener("paste", this.pasteEventListener);
- this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles);
+ this.appEvents.off(`${this.composerEventPrefix}:add-files`, this._addFiles);
this.appEvents.off(
- `${this.eventPrefix}:cancel-upload`,
+ `${this.composerEventPrefix}:cancel-upload`,
this._cancelSingleUpload
);
@@ -84,7 +85,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
},
_abortAndReset() {
- this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`);
+ this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`);
this._reset();
return false;
},
@@ -97,9 +98,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
this.fileInputEl = document.getElementById(this.fileUploadElementId);
const isPrivateMessage = this.get("composerModel.privateMessage");
- this.appEvents.on(`${this.eventPrefix}:add-files`, this._addFiles);
+ this.appEvents.on(`${this.composerEventPrefix}:add-files`, this._addFiles);
this.appEvents.on(
- `${this.eventPrefix}:cancel-upload`,
+ `${this.composerEventPrefix}:cancel-upload`,
this._cancelSingleUpload
);
@@ -136,7 +137,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
});
if (!isUploading) {
- this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`);
+ this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`);
}
return isUploading;
},
@@ -233,7 +234,10 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
if (reason === "cancel-all") {
return;
}
-
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:upload-cancelled`,
+ file.id
+ );
file.meta.cancelled = true;
this._removeInProgressUpload(file.id);
this._resetUpload(file, { removePlaceholder: true });
@@ -289,12 +293,16 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
this.placeholders[file.id] = {
uploadPlaceholder: placeholder,
};
+
+ if (this.useUploadPlaceholders) {
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:insert-text`,
+ placeholder
+ );
+ }
+
this.appEvents.trigger(
- `${this.eventPrefix}:insert-text`,
- placeholder
- );
- this.appEvents.trigger(
- `${this.eventPrefix}:upload-started`,
+ `${this.composerEventPrefix}:upload-started`,
file.name
);
});
@@ -315,15 +323,17 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
cacheShortUploadUrl(upload.short_url, upload);
- this.appEvents.trigger(
- `${this.eventPrefix}:replace-text`,
- this.placeholders[file.id].uploadPlaceholder.trim(),
- markdown
- );
+ if (this.useUploadPlaceholders) {
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:replace-text`,
+ this.placeholders[file.id].uploadPlaceholder.trim(),
+ markdown
+ );
+ }
this._resetUpload(file, { removePlaceholder: false });
this.appEvents.trigger(
- `${this.eventPrefix}:upload-success`,
+ `${this.composerEventPrefix}:upload-success`,
file.name,
upload
);
@@ -334,7 +344,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
this._uppyInstance.on("complete", () => {
run(() => {
- this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`);
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:all-uploads-complete`
+ );
this._reset();
});
});
@@ -345,18 +357,20 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
if (this.userCancelled) {
Object.values(this.placeholders).forEach((data) => {
run(() => {
- this.appEvents.trigger(
- `${this.eventPrefix}:replace-text`,
- data.uploadPlaceholder,
- ""
- );
+ if (this.useUploadPlaceholders) {
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:replace-text`,
+ data.uploadPlaceholder,
+ ""
+ );
+ }
});
});
this.set("userCancelled", false);
this._reset();
- this.appEvents.trigger(`${this.eventPrefix}:uploads-cancelled`);
+ this.appEvents.trigger(`${this.composerEventPrefix}:uploads-cancelled`);
}
});
@@ -381,7 +395,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
if (!this.userCancelled) {
displayErrorForUpload(response || error, this.siteSettings, file.name);
- this.appEvents.trigger(`${this.eventPrefix}:upload-error`, file);
+ this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file);
}
if (this.inProgressUploads.length === 0) {
@@ -434,7 +448,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
)}]()\n`;
this.appEvents.trigger(
- `${this.eventPrefix}:replace-text`,
+ `${this.composerEventPrefix}:replace-text`,
placeholderData.uploadPlaceholder,
placeholderData.processingPlaceholder
);
@@ -445,7 +459,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
run(() => {
let placeholderData = this.placeholders[file.id];
this.appEvents.trigger(
- `${this.eventPrefix}:replace-text`,
+ `${this.composerEventPrefix}:replace-text`,
placeholderData.processingPlaceholder,
placeholderData.uploadPlaceholder
);
@@ -458,7 +472,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
isCancellable: true,
});
this.appEvents.trigger(
- `${this.eventPrefix}:uploads-preprocessing-complete`
+ `${this.composerEventPrefix}:uploads-preprocessing-complete`
);
});
}
@@ -536,7 +550,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
_resetUpload(file, opts) {
if (opts.removePlaceholder) {
this.appEvents.trigger(
- `${this.eventPrefix}:replace-text`,
+ `${this.composerEventPrefix}:replace-text`,
this.placeholders[file.id].uploadPlaceholder,
""
);
diff --git a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js
index 9bcf1b54af..ca2a312d3b 100644
--- a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js
+++ b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js
@@ -92,7 +92,7 @@ export default Mixin.create(UploadDebugging, {
});
},
- _onPreProcessComplete(callback, allCompleteCallback) {
+ _onPreProcessComplete(callback, allCompleteCallback = null) {
this._uppyInstance.on("preprocess-complete", (file, skipped, pluginId) => {
this._consoleDebug(
`[${pluginId}] ${skipped ? "skipped" : "completed"} processing file ${
@@ -105,7 +105,9 @@ export default Mixin.create(UploadDebugging, {
this._completePreProcessing(pluginId, (allComplete) => {
if (allComplete) {
this._consoleDebug("[uppy] All upload preprocessors complete!");
- allCompleteCallback();
+ if (allCompleteCallback) {
+ allCompleteCallback();
+ }
}
});
});
diff --git a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js
index ab6aeab38a..b5a3e49512 100644
--- a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js
+++ b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js
@@ -34,6 +34,13 @@ export function getHead(head, prev) {
export default Mixin.create({
init() {
this._super(...arguments);
+
+ // fallback in the off chance someone has implemented a custom composer
+ // which does not define this
+ if (!this.composerEventPrefix) {
+ this.composerEventPrefix = "composer";
+ }
+
generateLinkifyFunction(this.markdownOptions || {}).then((linkify) => {
// When pasting links, we should use the same rules to match links as we do when creating links for a cooked post.
this._cachedLinkify = linkify;
@@ -41,14 +48,7 @@ export default Mixin.create({
},
// ensures textarea scroll position is correct
- //
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
focusTextArea() {
- this._focusTextArea();
- },
-
- _focusTextArea() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
@@ -61,33 +61,15 @@ export default Mixin.create({
this._textarea.focus();
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
insertBlock(text) {
- this._insertBlock(text);
- },
-
- _insertBlock(text) {
this._addBlock(this.getSelected(), text);
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
insertText(text, options) {
- this._insertText(text, options);
+ this.addText(this.getSelected(), text, options);
},
- _insertText(text, options) {
- this._addText(this.getSelected(), text, options);
- },
-
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
getSelected(trimLeading, opts) {
- return this._getSelected(trimLeading, opts);
- },
-
- _getSelected(trimLeading, opts) {
if (!this.ready || !this.element) {
return;
}
@@ -114,7 +96,7 @@ export default Mixin.create({
if (opts && opts.lineVal) {
const lineVal = value.split("\n")[
- value.substr(0, this._textarea.selectionStart).split("\n").length - 1
+ value.slice(0, this._textarea.selectionStart).split("\n").length - 1
];
return { start, end, value: selVal, pre, post, lineVal };
} else {
@@ -122,13 +104,7 @@ export default Mixin.create({
}
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
selectText(from, length, opts = { scroll: true }) {
- this._selectText(from, length, opts);
- },
-
- _selectText(from, length, opts = { scroll: true }) {
next(() => {
if (!this.element) {
return;
@@ -147,13 +123,7 @@ export default Mixin.create({
});
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
replaceText(oldVal, newVal, opts = {}) {
- this._replaceText(oldVal, newVal, opts);
- },
-
- _replaceText(oldVal, newVal, opts = {}) {
const val = this.value;
const needleStart = val.indexOf(oldVal);
@@ -196,13 +166,7 @@ export default Mixin.create({
}
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
applySurround(sel, head, tail, exampleKey, opts) {
- this._applySurround(sel, head, tail, exampleKey, opts);
- },
-
- _applySurround(sel, head, tail, exampleKey, opts) {
const pre = sel.pre;
const post = sel.post;
@@ -337,16 +301,10 @@ export default Mixin.create({
this._$textarea.prop("selectionStart", (pre + text).length + 2);
this._$textarea.prop("selectionEnd", (pre + text).length + 2);
- schedule("afterRender", this, this._focusTextArea);
+ schedule("afterRender", this, this.focusTextArea);
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
addText(sel, text, options) {
- this._addText(sel, text, options);
- },
-
- _addText(sel, text, options) {
if (options && options.ensureSpace) {
if ((sel.pre + "").length > 0) {
if (!sel.pre.match(/\s$/)) {
@@ -367,16 +325,10 @@ export default Mixin.create({
this._$textarea.prop("selectionStart", insert.length);
this._$textarea.prop("selectionEnd", insert.length);
next(() => this._$textarea.trigger("change"));
- this._focusTextArea();
+ this.focusTextArea();
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
extractTable(text) {
- return this._extractTable(text);
- },
-
- _extractTable(text) {
if (text.endsWith("\n")) {
text = text.substring(0, text.length - 1);
}
@@ -413,13 +365,7 @@ export default Mixin.create({
return null;
},
- // TODO (martin) clean up this indirection, functions used outside this
- // file should not be prefixed with lowercase
isInside(text, regex) {
- return this._isInside(text, regex);
- },
-
- _isInside(text, regex) {
const matches = text.match(regex);
return matches && matches.length % 2;
},
@@ -445,7 +391,7 @@ export default Mixin.create({
const selected = this.getSelected(null, { lineVal: true });
const { pre, value: selectedValue, lineVal } = selected;
const isInlinePasting = pre.match(/[^\n]$/);
- const isCodeBlock = this._isInside(pre, /(^|\n)```/g);
+ const isCodeBlock = this.isInside(pre, /(^|\n)```/g);
if (
plainText &&
@@ -454,9 +400,12 @@ export default Mixin.create({
!isCodeBlock
) {
plainText = plainText.replace(/\r/g, "");
- const table = this._extractTable(plainText);
+ const table = this.extractTable(plainText);
if (table) {
- this.appEvents.trigger("composer:insert-text", table);
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:insert-text`,
+ table
+ );
handled = true;
}
}
@@ -465,7 +414,7 @@ export default Mixin.create({
if (isInlinePasting) {
canPasteHtml = !(
lineVal.match(/^```/) ||
- this._isInside(pre, /`/g) ||
+ this.isInside(pre, /`/g) ||
lineVal.match(/^ /)
);
} else {
@@ -492,7 +441,7 @@ export default Mixin.create({
) {
// When specified, linkify supports fuzzy links and emails. Prefer providing the protocol.
// eg: pasting "example@discourse.org" may apply a link format of "mailto:example@discourse.org"
- this._addText(selected, `[${selectedValue}](${match.url})`);
+ this.addText(selected, `[${selectedValue}](${match.url})`);
handled = true;
}
}
@@ -508,7 +457,10 @@ export default Mixin.create({
}
if (isComposer) {
- this.appEvents.trigger("composer:insert-text", markdown);
+ this.appEvents.trigger(
+ `${this.composerEventPrefix}:insert-text`,
+ markdown
+ );
handled = true;
}
}
@@ -614,9 +566,9 @@ export default Mixin.create({
if (isEmpty(captures)) {
if (selected.pre.match(/\S$/)) {
- this._addText(selected, ` :${code}:`);
+ this.addText(selected, ` :${code}:`);
} else {
- this._addText(selected, `:${code}:`);
+ this.addText(selected, `:${code}:`);
}
} else {
let numOfRemovedChars = selected.pre.length - captures[1].length;
@@ -626,7 +578,7 @@ export default Mixin.create({
);
selected.start -= numOfRemovedChars;
selected.end -= numOfRemovedChars;
- this._addText(selected, `${code}:`);
+ this.addText(selected, `${code}:`);
}
},
});
diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js
index aaf6b4ec99..0807f98f40 100644
--- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js
+++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js
@@ -1,4 +1,6 @@
import Mixin from "@ember/object/mixin";
+import { run } from "@ember/runloop";
+import ExtendableUploader from "discourse/mixins/extendable-uploader";
import { or } from "@ember/object/computed";
import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
@@ -23,7 +25,7 @@ import bootbox from "bootbox";
export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB
-export default Mixin.create(UppyS3Multipart, {
+export default Mixin.create(UppyS3Multipart, ExtendableUploader, {
uploading: false,
uploadProgress: 0,
_uppyInstance: null,
@@ -55,6 +57,10 @@ export default Mixin.create(UppyS3Multipart, {
this.fileInputEventListener
);
this.appEvents.off(`upload-mixin:${this.id}:add-files`, this._addFiles);
+ this.appEvents.off(
+ `upload-mixin:${this.id}:cancel-upload`,
+ this._cancelSingleUpload
+ );
this._uppyInstance?.close();
this._uppyInstance = null;
},
@@ -66,6 +72,7 @@ export default Mixin.create(UppyS3Multipart, {
});
this.set("allowMultipleFiles", this.fileInputEl.multiple);
this.set("inProgressUploads", []);
+ this._triggerInProgressUploadsEvent();
this._bindFileInputChange();
@@ -105,6 +112,7 @@ export default Mixin.create(UppyS3Multipart, {
uploadProgress: 0,
uploading: isValid && this.autoStartUploads,
filesAwaitingUpload: !this.autoStartUploads,
+ cancellable: isValid && this.autoStartUploads,
});
return isValid;
},
@@ -141,8 +149,8 @@ export default Mixin.create(UppyS3Multipart, {
},
});
+ // droptarget is a UI plugin, only preprocessors must call _useUploadPlugin
this._uppyInstance.use(DropTarget, this._uploadDropTargetOptions());
- this._uppyInstance.use(UppyChecksum, { capabilities: this.capabilities });
this._uppyInstance.on("progress", (progress) => {
if (this.isDestroying || this.isDestroyed) {
@@ -153,48 +161,78 @@ export default Mixin.create(UppyS3Multipart, {
});
this._uppyInstance.on("upload", (data) => {
+ this._addNeedProcessing(data.fileIDs.length);
const files = data.fileIDs.map((fileId) =>
this._uppyInstance.getFile(fileId)
);
+ this.setProperties({
+ processing: true,
+ cancellable: false,
+ });
files.forEach((file) => {
- this.inProgressUploads.push(
+ // The inProgressUploads is meant to be used to display these uploads
+ // in a UI, and Ember will only update the array in the UI if pushObject
+ // is used to notify it.
+ this.inProgressUploads.pushObject(
EmberObject.create({
fileName: file.name,
id: file.id,
progress: 0,
+ extension: file.extension,
+ processing: false,
})
);
+ this._triggerInProgressUploadsEvent();
+ });
+ });
+
+ this._uppyInstance.on("upload-progress", (file, progress) => {
+ run(() => {
+ if (this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
+ const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
+ if (upload) {
+ const percentage = Math.round(
+ (progress.bytesUploaded / progress.bytesTotal) * 100
+ );
+ upload.set("progress", percentage);
+ }
});
});
this._uppyInstance.on("upload-success", (file, response) => {
- this._removeInProgressUpload(file.id);
-
if (this.usingS3Uploads) {
this.setProperties({ uploading: false, processing: true });
this._completeExternalUpload(file)
.then((completeResponse) => {
+ this._removeInProgressUpload(file.id);
+ this.appEvents.trigger(
+ `upload-mixin:${this.id}:upload-success`,
+ file.name,
+ completeResponse
+ );
this.uploadDone(
deepMerge(completeResponse, { file_name: file.name })
);
- if (this.inProgressUploads.length === 0) {
- this._reset();
- }
+ this._triggerInProgressUploadsEvent();
})
.catch((errResponse) => {
displayErrorForUpload(errResponse, this.siteSettings, file.name);
- if (this.inProgressUploads.length === 0) {
- this._reset();
- }
+ this._triggerInProgressUploadsEvent();
});
} else {
- this.uploadDone(
- deepMerge(response?.body || {}, { file_name: file.name })
+ this._removeInProgressUpload(file.id);
+ const upload = response?.body || {};
+ this.appEvents.trigger(
+ `upload-mixin:${this.id}:upload-success`,
+ file.name,
+ upload
);
- if (this.inProgressUploads.length === 0) {
- this._reset();
- }
+ this.uploadDone(deepMerge(upload, { file_name: file.name }));
+ this._triggerInProgressUploadsEvent();
}
});
@@ -204,6 +242,28 @@ export default Mixin.create(UppyS3Multipart, {
this._reset();
});
+ this._uppyInstance.on("file-removed", (file, reason) => {
+ run(() => {
+ // we handle the cancel-all event specifically, so no need
+ // to do anything here. this event is also fired when some files
+ // are handled by an upload handler
+ if (reason === "cancel-all") {
+ return;
+ }
+ this.appEvents.trigger(
+ `upload-mixin:${this.id}:upload-cancelled`,
+ file.id
+ );
+ });
+ });
+
+ this._uppyInstance.on("complete", () => {
+ run(() => {
+ this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`);
+ this._reset();
+ });
+ });
+
// TODO (martin) preventDirectS3Uploads is necessary because some of
// the current upload mixin components, for example the emoji uploader,
// send the upload to custom endpoints that do fancy things in the rails
@@ -228,8 +288,33 @@ export default Mixin.create(UppyS3Multipart, {
}
}
+ this._uppyInstance.on("cancel-all", () => {
+ this.appEvents.trigger(`upload-mixin:${this.id}:uploads-cancelled`);
+ if (!this.isDestroyed && !this.isDestroying) {
+ this.set("inProgressUploads", []);
+ this._triggerInProgressUploadsEvent();
+ }
+ });
+
this.appEvents.on(`upload-mixin:${this.id}:add-files`, this._addFiles);
+ this.appEvents.on(
+ `upload-mixin:${this.id}:cancel-upload`,
+ this._cancelSingleUpload
+ );
this._uppyReady();
+
+ // It is important that the UppyChecksum preprocessor is the last one to
+ // be added; the preprocessors are run in order and since other preprocessors
+ // may modify the file (e.g. the UppyMediaOptimization one), we need to
+ // checksum once we are sure the file data has "settled".
+ this._useUploadPlugin(UppyChecksum, { capabilities: this.capabilities });
+ },
+
+ _triggerInProgressUploadsEvent() {
+ this.appEvents.trigger(
+ `upload-mixin:${this.id}:in-progress-uploads`,
+ this.inProgressUploads
+ );
},
// This should be overridden in a child component if you need to
@@ -325,6 +410,12 @@ export default Mixin.create(UppyS3Multipart, {
);
},
+ @bind
+ _cancelSingleUpload(data) {
+ this._uppyInstance.removeFile(data.fileId);
+ this._removeInProgressUpload(data.fileId);
+ },
+
@bind
_addFiles(files, opts = {}) {
files = Array.isArray(files) ? files : [files];
@@ -362,6 +453,7 @@ export default Mixin.create(UppyS3Multipart, {
this.setProperties({
uploading: false,
processing: false,
+ cancellable: false,
uploadProgress: 0,
filesAwaitingUpload: false,
});
@@ -369,10 +461,15 @@ export default Mixin.create(UppyS3Multipart, {
},
_removeInProgressUpload(fileId) {
+ if (this.isDestroyed || this.isDestroying) {
+ return;
+ }
+
this.set(
"inProgressUploads",
this.inProgressUploads.filter((upl) => upl.id !== fileId)
);
+ this._triggerInProgressUploadsEvent();
},
// target must be provided as a DOM element, however the
diff --git a/app/assets/javascripts/discourse/app/models/bookmark.js b/app/assets/javascripts/discourse/app/models/bookmark.js
index 8852409a77..f28c92845e 100644
--- a/app/assets/javascripts/discourse/app/models/bookmark.js
+++ b/app/assets/javascripts/discourse/app/models/bookmark.js
@@ -38,6 +38,14 @@ const Bookmark = RestModel.extend({
},
attachedTo() {
+ if (this.siteSettings.use_polymorphic_bookmarks) {
+ return {
+ target: this.bookmarkable_type.toLowerCase(),
+ targetId: this.bookmarkable_id,
+ };
+ }
+
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
if (this.for_topic) {
return { target: "topic", targetId: this.topic_id };
}
diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js
index 1ff851355f..e4435bd8b1 100644
--- a/app/assets/javascripts/discourse/app/models/category.js
+++ b/app/assets/javascripts/discourse/app/models/category.js
@@ -35,21 +35,13 @@ const Category = RestModel.extend({
}
},
- @on("init")
- setupRequiredTagGroups() {
- if (this.required_tag_group_name) {
- this.set("required_tag_groups", [this.required_tag_group_name]);
- }
- },
-
- @discourseComputed(
- "required_tag_groups",
- "min_tags_from_required_group",
- "minimum_required_tags"
- )
+ @discourseComputed("required_tag_groups", "minimum_required_tags")
minimumRequiredTags() {
- if (this.required_tag_groups) {
- return this.min_tags_from_required_group;
+ if (this.required_tag_groups?.length > 0) {
+ return this.required_tag_groups.reduce(
+ (sum, rtg) => sum + rtg.min_count,
+ 0
+ );
} else {
return this.minimum_required_tags > 0 ? this.minimum_required_tags : null;
}
@@ -200,7 +192,8 @@ const Category = RestModel.extend({
const url = id ? `/categories/${id}` : "/categories";
return ajax(url, {
- data: {
+ contentType: "application/json",
+ data: JSON.stringify({
name: this.name,
slug: this.slug,
color: this.color,
@@ -225,20 +218,10 @@ const Category = RestModel.extend({
all_topics_wiki: this.all_topics_wiki,
allow_unlimited_owner_edits_on_first_post: this
.allow_unlimited_owner_edits_on_first_post,
- allowed_tags:
- this.allowed_tags && this.allowed_tags.length > 0
- ? this.allowed_tags
- : null,
- allowed_tag_groups:
- this.allowed_tag_groups && this.allowed_tag_groups.length > 0
- ? this.allowed_tag_groups
- : null,
+ allowed_tags: this.allowed_tags,
+ allowed_tag_groups: this.allowed_tag_groups,
allow_global_tags: this.allow_global_tags,
- required_tag_group_name:
- this.required_tag_groups && this.required_tag_groups.length > 0
- ? this.required_tag_groups[0]
- : null,
- min_tags_from_required_group: this.min_tags_from_required_group,
+ required_tag_groups: this.required_tag_groups,
sort_order: this.sort_order,
sort_ascending: this.sort_ascending,
topic_featured_link_allowed: this.topic_featured_link_allowed,
@@ -255,7 +238,7 @@ const Category = RestModel.extend({
reviewable_by_group_name: this.reviewable_by_group_name,
read_only_banner: this.read_only_banner,
default_list_filter: this.default_list_filter,
- },
+ }),
type: id ? "PUT" : "POST",
});
},
diff --git a/app/assets/javascripts/discourse/app/models/invite.js b/app/assets/javascripts/discourse/app/models/invite.js
index 122208bc1b..6608ecb079 100644
--- a/app/assets/javascripts/discourse/app/models/invite.js
+++ b/app/assets/javascripts/discourse/app/models/invite.js
@@ -36,7 +36,7 @@ const Invite = EmberObject.extend({
@discourseComputed("invite_key")
shortKey(key) {
- return key.substr(0, 4) + "...";
+ return key.slice(0, 4) + "...";
},
@discourseComputed("groups")
diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js
index 685750b52e..050d0247eb 100644
--- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js
+++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js
@@ -441,7 +441,7 @@ const TopicTrackingState = EmberObject.extend({
},
_generateCallbackId() {
- return Math.random().toString(12).substr(2, 9);
+ return Math.random().toString(12).slice(2, 11);
},
onStateChange(cb) {
diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js
index ae74d2ea63..e6b960f4ea 100644
--- a/app/assets/javascripts/discourse/app/models/topic.js
+++ b/app/assets/javascripts/discourse/app/models/topic.js
@@ -41,6 +41,7 @@ export function loadTopicView(topic, args) {
}
export const ID_CONSTRAINT = /^\d+$/;
+let _customLastUnreadUrlCallbacks = [];
const Topic = RestModel.extend({
message: null,
@@ -256,6 +257,19 @@ const Topic = RestModel.extend({
@discourseComputed("last_read_post_number", "highest_post_number", "url")
lastUnreadUrl(lastReadPostNumber, highestPostNumber) {
+ let customUrl = null;
+ _customLastUnreadUrlCallbacks.some((cb) => {
+ const result = cb(this);
+ if (result) {
+ customUrl = result;
+ return true;
+ }
+ });
+
+ if (customUrl) {
+ return customUrl;
+ }
+
if (highestPostNumber <= lastReadPostNumber) {
if (this.get("category.navigate_to_first_post_after_read")) {
return this.urlForPostNumber(1);
@@ -383,7 +397,15 @@ const Topic = RestModel.extend({
this.set(
"bookmarks",
this.bookmarks.filter((bookmark) => {
- if (bookmark.id === id && bookmark.for_topic) {
+ // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented.
+ if (
+ (!this.siteSettings.use_polymorphic_bookmarks &&
+ bookmark.id === id &&
+ bookmark.for_topic) ||
+ (this.siteSettings.use_polymorphic_bookmarks &&
+ bookmark.id === id &&
+ bookmark.bookmarkable_type === "Topic")
+ ) {
// TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed.
this.appEvents.trigger("topic:bookmark-toggled");
this.appEvents.trigger(
@@ -403,7 +425,11 @@ const Topic = RestModel.extend({
clearBookmarks() {
this.toggleProperty("bookmarked");
- const postIds = this.bookmarks.mapBy("post_id");
+ const postIds = this.siteSettings.use_polymorphic_bookmarks
+ ? this.bookmarks
+ .filterBy("bookmarkable_type", "Post")
+ .mapBy("bookmarkable_id")
+ : this.bookmarks.mapBy("post_id");
postIds.forEach((postId) => {
const loadedPost = this.postStream.findLoadedPost(postId);
if (loadedPost) {
@@ -548,7 +574,7 @@ const Topic = RestModel.extend({
@discourseComputed("excerpt")
excerptTruncated(excerpt) {
- return excerpt && excerpt.substr(excerpt.length - 8, 8) === "…";
+ return excerpt && excerpt.slice(-8) === "…";
},
readLastPost: propertyEqual("last_read_post_number", "highest_post_number"),
@@ -876,4 +902,13 @@ export function mergeTopic(topicId, data) {
);
}
+export function registerCustomLastUnreadUrlCallback(fn) {
+ _customLastUnreadUrlCallbacks.push(fn);
+}
+
+// Should only be used in tests
+export function clearCustomLastUnreadUrlCallbacks() {
+ _customLastUnreadUrlCallbacks.clear();
+}
+
export default Topic;
diff --git a/app/assets/javascripts/discourse/app/routes/discourse.js b/app/assets/javascripts/discourse/app/routes/discourse.js
index eb2b4f3c43..9890e5c2ca 100644
--- a/app/assets/javascripts/discourse/app/routes/discourse.js
+++ b/app/assets/javascripts/discourse/app/routes/discourse.js
@@ -74,6 +74,7 @@ const DiscourseRoute = Route.extend({
}
},
+ // deprecated, use isCurrentUser() instead
isAnotherUsersPage(user) {
if (!this.currentUser) {
return true;
@@ -82,6 +83,14 @@ const DiscourseRoute = Route.extend({
return user.username !== this.currentUser.username;
},
+ isCurrentUser(user) {
+ if (!this.currentUser) {
+ return false; // the current user is anonymous
+ }
+
+ return user.id === this.currentUser.id;
+ },
+
isPoppedState(transition) {
return !transition._discourse_intercepted && !!transition.intent.url;
},
diff --git a/app/assets/javascripts/discourse/app/routes/topic-from-params.js b/app/assets/javascripts/discourse/app/routes/topic-from-params.js
index b1f30baa5c..5875089d6d 100644
--- a/app/assets/javascripts/discourse/app/routes/topic-from-params.js
+++ b/app/assets/javascripts/discourse/app/routes/topic-from-params.js
@@ -92,7 +92,7 @@ export default DiscourseRoute.extend({
const opts = {};
if (document.location.hash) {
- opts.anchor = document.location.hash.substr(1);
+ opts.anchor = document.location.hash.slice(1);
} else if (_discourse_anchor) {
opts.anchor = _discourse_anchor;
}
diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js
index 0eaf79c8b0..67b73d3634 100644
--- a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js
+++ b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js
@@ -11,7 +11,7 @@ export default DiscourseRoute.extend({
return draftsStream.findItems(this.site).then(() => {
return {
stream: draftsStream,
- isAnotherUsersPage: this.isAnotherUsersPage(user),
+ isAnotherUsersPage: !this.isCurrentUser(user),
emptyState: this.emptyState(),
};
});
diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js
index 69fabb3173..f0e1287d37 100644
--- a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js
+++ b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js
@@ -16,7 +16,7 @@ export default DiscourseRoute.extend(ViewingActionType, {
return {
stream,
- isAnotherUsersPage: this.isAnotherUsersPage(user),
+ isAnotherUsersPage: !this.isCurrentUser(user),
emptyState: this.emptyState(),
emptyStateOthers: this.emptyStateOthers,
};
diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js
index 9e48ca30d5..bb8ee51b78 100644
--- a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js
+++ b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js
@@ -23,8 +23,15 @@ export default UserTopicListRoute.extend({
},
emptyState() {
+ const user = this.modelFor("user");
+ const title = this.isCurrentUser(user)
+ ? I18n.t("user_activity.no_topics_title")
+ : I18n.t("user_activity.no_topics_title_others", {
+ username: user.username,
+ });
+
return {
- title: I18n.t("user_activity.no_topics_title"),
+ title,
body: "",
};
},
diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
index e7b76e8bb9..44d14a340a 100644
--- a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
@@ -38,9 +38,9 @@
{{#if userHasTimezoneSet}}
{{time-shortcut-picker
+ timeShortcuts=timeOptions
prefilledDatetime=prefilledDatetime
onTimeSelected=(action "onTimeSelected")
- customOptions=customTimeShortcutOptions
hiddenOptions=hiddenTimeShortcutOptions
customLabels=customTimeShortcutLabels
_itsatrap=_itsatrap
diff --git a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
index 77aac380bc..767b0a854a 100644
--- a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
@@ -23,9 +23,5 @@
{{/d-editor}}
{{#if allowUpload}}
- {{#if acceptsAllFormats}}
-
- {{else}}
-
- {{/if}}
+ {{pick-files-button fileInputId="file-uploader" allowMultiple=true}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
index 8e16ab00a9..aa408070cb 100644
--- a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
@@ -15,10 +15,10 @@
value=category.parent_category_id
categories=parentCategories
allowSubCategories=true
- allowUncategorized=false
allowRestrictedCategories=true
onChange=(action (mut category.parent_category_id))
options=(hash
+ allowUncategorized=false
excludeCategoryId=category.id
none=true
)
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs
index 13d1031a4b..b093ec89e6 100644
--- a/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs
@@ -18,6 +18,7 @@
{{tag-group-chooser
id="category-allowed-tag-groups"
tagGroups=category.allowed_tag_groups
+ onChange=(action (mut category.allowed_tag_groups))
}}
{{#link-to "tagGroups" class="manage-tag-groups"}}{{i18n "category.manage_tag_groups_link"}}{{/link-to}}
@@ -34,23 +35,33 @@
- {{i18n "category.required_tag_group_description"}}
+ {{i18n "category.required_tag_group.description"}}
-
-
-
- {{tag-group-chooser
- id="category-required-tag-group"
- tagGroups=category.required_tag_groups
- options=(hash
- maximum=1
- filterPlaceholder="category.tag_group_selector_placeholder"
- )
- }}
+
+ {{#each category.required_tag_groups as |rtg|}}
+
+ {{text-field value=rtg.min_count type="number" min="1"}}
+ {{tag-group-chooser
+ tagGroups=(if rtg.name (array rtg.name) (array))
+ onChange=(action "onTagGroupChange" rtg)
+ options=(hash
+ maximum=1
+ filterPlaceholder="category.required_tag_group.placeholder"
+ )
+ }}
+ {{d-button
+ label="category.required_tag_group.delete"
+ action=(action "deleteRequiredTagGroup" rtg)
+ icon="trash-alt"
+ class="delete-required-tag-group"}}
+
+ {{/each}}
+ {{d-button
+ label="category.required_tag_group.add"
+ action=(action "addRequiredTagGroup")
+ icon="plus"
+ class="add-required-tag-group"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
index 6655b533f7..c9d6d28ed7 100644
--- a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
@@ -12,17 +12,19 @@
{{category-chooser
value=topicTimer.category_id
- excludeCategoryId=excludeCategoryId
onChange=(action (mut topicTimer.category_id))
+ options=(hash
+ excludeCategoryId=excludeCategoryId
+ )
}}
{{/if}}
{{#if showFutureDateInput}}
{{time-shortcut-picker
+ timeShortcuts=timeOptions
prefilledDatetime=topicTimer.execute_at
onTimeSelected=onTimeSelected
- customOptions=customTimeShortcutOptions
hiddenOptions=hiddenTimeShortcutOptions
_itsatrap=_itsatrap
}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs
index e4d19aaf30..9b66d8079f 100644
--- a/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs
@@ -28,9 +28,11 @@
value=group.imap_mailbox_name
valueProperty="value"
content=mailboxes
- none="groups.manage.email.mailboxes.disabled"
tabindex="10"
onChange=(action (mut group.imap_mailbox_name))
+ options=(hash
+ none="groups.manage.email.mailboxes.disabled"
+ )
}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
index 409a3becbd..5b91833292 100644
--- a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
@@ -1,6 +1,8 @@
-{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
-{{#if acceptAnyFile}}
-
-{{else}}
-
+{{#if showButton}}
+ {{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
+{{/if}}
+{{#if acceptsAllFormats}}
+
+{{else}}
+
{{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs b/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs
index c40ac432c3..17eb883752 100644
--- a/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs
@@ -2,13 +2,13 @@
{{dropdown-select-box
class="reviewable-action-dropdown"
nameProperty="label"
- title=bundle.label
content=bundle.actions
onChange=(action "performById")
options=(hash
icon=bundle.icon
disabled=reviewableUpdating
placement=placement
+ translatedNone=bundle.label
)
}}
{{else}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
index 5cafc9ede9..d3d0aa4aa0 100644
--- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
@@ -23,11 +23,11 @@
{{tag-chooser
id="search-with-tags"
tags=searchedTerms.tags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
onChange=(action "onChangeSearchTermForTags")
options=(hash
+ allowAny=false
headerAriaLabel=(i18n "search.advanced.with_tags.aria_label")
)
}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs
index ea93c26dbd..941ec29b38 100644
--- a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs
@@ -1,5 +1,11 @@
{{#each posts as |post|}}
- {{search-result-entry post=post bulkSelectEnabled=bulkSelectEnabled selected=selected highlightQuery=highlightQuery}}
+ {{search-result-entry
+ post=post
+ bulkSelectEnabled=bulkSelectEnabled
+ selected=selected
+ highlightQuery=highlightQuery
+ searchLogId=searchLogId
+ }}
{{/each}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs
index 4437c84545..3fabd1678d 100644
--- a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs
@@ -8,10 +8,10 @@
{{tag-chooser
tags=buffered.tag_names
everyTag=true
- allowCreate=true
unlimitedTagCount=true
excludeSynonyms=true
options=(hash
+ allowAny=true
filterPlaceholder="tagging.groups.tags_placeholder"
)
}}
@@ -23,11 +23,11 @@
{{tag-chooser
tags=buffered.parent_tag_name
everyTag=true
- maximum=1
- allowCreate=true
excludeSynonyms=true
options=(hash
+ allowAny=true
filterPlaceholder="tagging.groups.parent_tag_placeholder"
+ maximum=1
)
}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs
index c6e9c5cae2..b7947a4f14 100644
--- a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs
@@ -1,7 +1,7 @@
-{{#d-button action=(action "enterTop") class="btn-default full jump-top"}}
+{{#d-button action=(action "enterTop") class="btn-default full jump-top" ariaLabel="topic_entrance.sr_jump_top_button"}}
{{d-icon "step-backward"}} {{html-safe topDate}}
{{/d-button}}
-{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom"}}
+{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom" ariaLabel="topic_entrance.sr_jump_bottom_button"}}
{{html-safe bottomDate}} {{d-icon "step-forward"}}
{{/d-button}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs
index 3ff3f75731..3a554aea95 100644
--- a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs
@@ -42,7 +42,8 @@
lastVisitedTopic=lastVisitedTopic
selected=selected
lastChecked=lastChecked
- tagsForUser=tagsForUser}}
+ tagsForUser=tagsForUser
+ focusLastVisitedTopic=focusLastVisitedTopic}}
{{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}}
{{/each}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs b/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs
index 1038cfe06c..9244794936 100644
--- a/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs
@@ -5,7 +5,7 @@
{{#if imageUrl}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs
index e4ed951fe8..b9b85d5df9 100644
--- a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs
@@ -1,4 +1,5 @@
{{#if this.visible}}
+ {{plugin-outlet name="before-user-card-content" args=(hash user=this.user)}}
{{#if this.loading}}
@@ -73,6 +74,10 @@
label="user.private_message"}}
{{/if}}
+ {{plugin-outlet
+ name="user-card-below-message-button" connectorTagName="li"
+ args=(hash user=this.user close=(action "close"))
+ tagName=""}}
{{#if this.showFilter}}
{{d-button
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs b/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs
index f34fe5267b..540b3f9756 100644
--- a/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs
@@ -12,8 +12,10 @@
valueProperty=null
nameProperty=null
value=this.value
- none=this.noneLabel
onChange=(action (mut this.value))
+ options=(hash
+ none=this.noneLabel
+ )
}}
{{html-safe this.field.description}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs b/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs
index f81fa36fd4..250206d819 100644
--- a/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs
@@ -12,8 +12,10 @@
valueProperty=null
nameProperty=null
value=this.value
- none=this.noneLabel
onChange=(action (mut this.value))
+ options=(hash
+ none=this.noneLabel
+ )
}}
{{html-safe this.field.description}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs
index 86246ae820..eaf91b06e0 100644
--- a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs
@@ -1,14 +1,32 @@
-
-
+{{/if}}
{{@user.title}}
diff --git a/app/assets/javascripts/discourse/app/templates/composer.hbs b/app/assets/javascripts/discourse/app/templates/composer.hbs
index b447c883c8..71d136501c 100644
--- a/app/assets/javascripts/discourse/app/templates/composer.hbs
+++ b/app/assets/javascripts/discourse/app/templates/composer.hbs
@@ -118,8 +118,8 @@
{{category-chooser
value=model.categoryId
onChange=(action (mut model.categoryId))
- isDisabled=disableCategoryChooser
options=(hash
+ disabled=disableCategoryChooser
scopedCategoryId=scopedCategoryId
prioritizedCategoryId=prioritizedCategoryId
)
@@ -131,9 +131,9 @@
{{#if canEditTags}}
{{mini-tag-chooser
value=model.tags
- isDisabled=disableTagsChooser
onChange=(action (mut model.tags))
options=(hash
+ disabled=disableTagsChooser
categoryId=model.categoryId
minimum=model.minimumRequiredTags
)
diff --git a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs
index 3c2997c58c..946d599382 100644
--- a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs
+++ b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs
@@ -62,7 +62,9 @@ model=model showResetNew=showResetNew showDismissRead=showDismissRead resetNew=(
topics=model.topics
discoveryList=true
scrollOnLoad=true
- onScroll=discoveryTopicList.saveScrollPosition}}
+ onScroll=discoveryTopicList.saveScrollPosition
+ focusLastVisitedTopic=true
+ }}
{{/if}}
{{plugin-outlet name="after-topic-list" tagName="span" connectorTagName="div" args=(hash category=category)}}
diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
index 4669c11f3e..1fb8cb4076 100644
--- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
+++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
@@ -131,6 +131,7 @@
bulkSelectEnabled=bulkSelectEnabled
selected=selected
highlightQuery=highlightQuery
+ searchLogId=model.grouped_search_result.search_log_id
}}
{{#conditional-loading-spinner condition=loading}}
diff --git a/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs b/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs
index 3b1206b178..74a2ba061d 100644
--- a/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs
+++ b/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs
@@ -13,9 +13,11 @@
{{tag-chooser
tags=model.watching_tags
blacklist=selectedTags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
+ options=(hash
+ allowAny=false
+ )
}}
@@ -29,9 +31,11 @@
{{tag-chooser
tags=model.tracking_tags
blacklist=selectedTags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
+ options=(hash
+ allowAny=false
+ )
}}
@@ -45,9 +49,11 @@
{{tag-chooser
tags=model.watching_first_post_tags
blacklist=selectedTags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
+ options=(hash
+ allowAny=false
+ )
}}
@@ -61,9 +67,11 @@
{{tag-chooser
tags=model.regular_tags
blacklist=selectedTags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
+ options=(hash
+ allowAny=false
+ )
}}
@@ -77,9 +85,11 @@
{{tag-chooser
tags=model.muted_tags
blacklist=selectedTags
- allowCreate=false
everyTag=true
unlimitedTagCount=true
+ options=(hash
+ allowAny=false
+ )
}}
diff --git a/app/assets/javascripts/discourse/app/templates/invites/show.hbs b/app/assets/javascripts/discourse/app/templates/invites/show.hbs
index cda2c35542..a5554b07dd 100644
--- a/app/assets/javascripts/discourse/app/templates/invites/show.hbs
+++ b/app/assets/javascripts/discourse/app/templates/invites/show.hbs
@@ -97,10 +97,11 @@
{{password-field value=accountPassword class=(value-entered accountPassword) type="password" id="new-account-password" capsLockOn=capsLockOn}}
{{input-tip validation=passwordValidation}}
- {{passwordInstructions}} {{i18n "invites.optional_description"}}
+ {{passwordInstructions}}
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
diff --git a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr
index a697ff9e01..0087b01566 100644
--- a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr
+++ b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr
@@ -1,5 +1,5 @@
<{{view.tagName}} class='num posts-map posts {{view.likesHeat}} topic-list-data' title='{{view.title}}'>
-