Version bump
This commit is contained in:
commit
c3a2561121
@ -16,3 +16,4 @@ app/assets/javascripts/discourse/tests/test-boot-rails.js
|
||||
app/assets/javascripts/discourse/tests/fixtures
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
||||
|
||||
2
.github/workflows/ember_with_plugins.yml
vendored
2
.github/workflows/ember_with_plugins.yml
vendored
@ -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 }}
|
||||
|
||||
4
.github/workflows/linting.yml
vendored
4
.github/workflows/linting.yml
vendored
@ -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 }}
|
||||
|
||||
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
4
Gemfile
4
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
|
||||
|
||||
42
Gemfile.lock
42
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
|
||||
|
||||
1
app/assets/config/manifest.js
Normal file
1
app/assets/config/manifest.js
Normal file
@ -0,0 +1 @@
|
||||
//= link_tree ../images
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -68,7 +68,9 @@
|
||||
value=buffered.badge_type_id
|
||||
content=badgeTypes
|
||||
onChange=(action (mut buffered.badge_type_id))
|
||||
isDisabled=readOnly
|
||||
options=(hash
|
||||
disabled=readOnly
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
@ -155,7 +157,9 @@
|
||||
value=buffered.trigger
|
||||
content=badgeTriggers
|
||||
onChange=(action (mut buffered.trigger))
|
||||
disabled=readOnly
|
||||
options=(hash
|
||||
disabled=readOnly
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -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"
|
||||
)
|
||||
}}
|
||||
|
||||
@ -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"
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{{category-chooser
|
||||
value=value
|
||||
allowUncategorized=true
|
||||
onChange=(action (mut value))
|
||||
options=(hash
|
||||
allowUncategorized=true
|
||||
none=(eq setting.default "")
|
||||
)
|
||||
}}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}}
|
||||
|
||||
@ -159,10 +159,12 @@
|
||||
<div class="setting-value">
|
||||
{{color-palettes
|
||||
content=colorSchemes
|
||||
filterable=true
|
||||
forceEscape=true
|
||||
value=colorSchemeId
|
||||
icon="paint-brush"}}
|
||||
icon="paint-brush"
|
||||
options=(hash
|
||||
filterable=true
|
||||
)
|
||||
}}
|
||||
|
||||
<div class="desc">{{i18n "admin.customize.theme.color_scheme_select"}}</div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<th>{{i18n "admin.email.time"}}</th>
|
||||
<th>{{i18n "admin.email.user"}}</th>
|
||||
<th>{{i18n "admin.email.to_address"}}</th>
|
||||
<th>{{i18n "admin.email.email_type"}}</th>
|
||||
<th colspan="2">{{i18n "admin.email.email_type"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -13,7 +13,7 @@
|
||||
<td>{{i18n "admin.email.logs.filters.title"}}</td>
|
||||
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
|
||||
<td colspan="2">{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each model as |l|}}
|
||||
@ -28,15 +28,26 @@
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="email-address"><a href="mailto:{{l.to_address}}">{{l.to_address}}</a></td>
|
||||
{{#if l.has_bounce_key}}
|
||||
<td><a href {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
|
||||
{{else}}
|
||||
<td>{{l.email_type}}</td>
|
||||
{{/if}}
|
||||
<td>
|
||||
{{#if l.has_bounce_key}}
|
||||
<a href {{action "showIncomingEmail" l.id}}>
|
||||
{{l.email_type}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{l.email_type}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="email-details">
|
||||
{{#if l.has_bounce_key}}
|
||||
<a href {{action "showIncomingEmail" l.id}} title={{i18n "admin.email.details_title"}}>
|
||||
{{d-icon "info-circle"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
{{#unless loading}}
|
||||
<tr><td colspan="4">{{i18n "admin.email.logs.none"}}</td></tr>
|
||||
<tr><td colspan="5">{{i18n "admin.email.logs.none"}}</td></tr>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<th>{{i18n "admin.email.incoming_emails.from_address"}}</th>
|
||||
<th>{{i18n "admin.email.incoming_emails.to_addresses"}}</th>
|
||||
<th>{{i18n "admin.email.incoming_emails.subject"}}</th>
|
||||
<th>{{i18n "admin.email.incoming_emails.error"}}</th>
|
||||
<th colspan="2">{{i18n "admin.email.incoming_emails.error"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
|
||||
<td colspan="2">{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each model as |email|}}
|
||||
@ -50,9 +50,14 @@
|
||||
<td class="error">
|
||||
<a href {{action "showIncomingEmail" email.id}}>{{email.error}}</a>
|
||||
</td>
|
||||
<td class="email-details">
|
||||
<a href {{action "showIncomingEmail" email.id}} title={{i18n "admin.email.details_title"}}>
|
||||
{{d-icon "info-circle"}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n "admin.email.incoming_emails.none"}}</td></tr>
|
||||
<tr><td colspan="6">{{i18n "admin.email.incoming_emails.none"}}</td></tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -17,10 +17,12 @@
|
||||
<div class="control-group">
|
||||
<label>{{i18n "admin.badges.badge"}}</label>
|
||||
{{combo-box
|
||||
filterable=true
|
||||
value=selectedBadgeId
|
||||
content=grantableBadges
|
||||
onChange=(action (mut selectedBadgeId))
|
||||
options=(hash
|
||||
filterable=true
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="control-group">
|
||||
|
||||
@ -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"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
{{#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}}
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ export function getURLWithCDN(url) {
|
||||
}
|
||||
|
||||
export function getAbsoluteURL(path) {
|
||||
return baseUrl + path;
|
||||
return baseUrl + withoutPrefix(path);
|
||||
}
|
||||
|
||||
export function isAbsoluteURL(url) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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));
|
||||
},
|
||||
},
|
||||
|
||||
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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));
|
||||
},
|
||||
});
|
||||
@ -18,7 +18,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||
this.set("loading", true);
|
||||
this.model
|
||||
.updateNotificationLevel({
|
||||
level: "ignored",
|
||||
level: "ignore",
|
||||
expiringAt: this.ignoredUntil,
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -111,7 +111,7 @@ export default Controller.extend({
|
||||
if (newList.length === 0) {
|
||||
this.refreshModel();
|
||||
} else {
|
||||
this.set("reviewables", newList);
|
||||
this.reviewables.setObjects(newList);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 `<img`.
|
||||
// HOWEVER that aspect ratio only applies while the image is loading. Once loaded, it'll use the
|
||||
// image's actual dimensions. This can cause things to jump around after loading. For example:
|
||||
// - if a user deliberately inserts false width/height
|
||||
// - the image fails to load (404)
|
||||
// - an optimised image is a few pixels different, due to a rounding when resizing
|
||||
//
|
||||
// This decorator explicitly sets the `aspect-ratio` property so that things are consistent throughout
|
||||
// the lifetime of all `<img` elements.
|
||||
|
||||
export default {
|
||||
name: "image-aspect-ratio",
|
||||
|
||||
initWithApi(api) {
|
||||
const supportsAspectRatio = CSS.supports("aspect-ratio: 1");
|
||||
|
||||
api.decorateCookedElement(
|
||||
(element) => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -772,7 +772,7 @@ export default {
|
||||
},
|
||||
|
||||
_onScrollEndsCallback() {
|
||||
document.querySelector(".topic-post.selected a.tabLoc")?.focus();
|
||||
document.querySelector(".topic-post.selected span.tabLoc")?.focus();
|
||||
},
|
||||
|
||||
categoriesTopicsList() {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()
|
||||
|
||||
11
app/assets/javascripts/discourse/app/lib/local-dates.js
Normal file
11
app/assets/javascripts/discourse/app/lib/local-dates.js
Normal file
@ -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);
|
||||
}
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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]));
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
""
|
||||
);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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}:`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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",
|
||||
});
|
||||
},
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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: "",
|
||||
};
|
||||
},
|
||||
|
||||
@ -38,9 +38,9 @@
|
||||
|
||||
{{#if userHasTimezoneSet}}
|
||||
{{time-shortcut-picker
|
||||
timeShortcuts=timeOptions
|
||||
prefilledDatetime=prefilledDatetime
|
||||
onTimeSelected=(action "onTimeSelected")
|
||||
customOptions=customTimeShortcutOptions
|
||||
hiddenOptions=hiddenTimeShortcutOptions
|
||||
customLabels=customTimeShortcutLabels
|
||||
_itsatrap=_itsatrap
|
||||
|
||||
@ -23,9 +23,5 @@
|
||||
{{/d-editor}}
|
||||
|
||||
{{#if allowUpload}}
|
||||
{{#if acceptsAllFormats}}
|
||||
<input type="file" id="file-uploader" multiple>
|
||||
{{else}}
|
||||
<input type="file" id="file-uploader" accept={{acceptedFormats}} multiple>
|
||||
{{/if}}
|
||||
{{pick-files-button fileInputId="file-uploader" allowMultiple=true}}
|
||||
{{/if}}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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}}
|
||||
</section>
|
||||
@ -34,23 +35,33 @@
|
||||
</section>
|
||||
|
||||
<section class="field required-tag-group-description">
|
||||
{{i18n "category.required_tag_group_description"}}
|
||||
{{i18n "category.required_tag_group.description"}}
|
||||
</section>
|
||||
|
||||
<section class="field with-items">
|
||||
<section class="field-item min-tags-from-required-group">
|
||||
<label for="category-min-tags-from-group">{{i18n "category.min_tags_from_required_group_label"}}</label>
|
||||
{{text-field value=category.min_tags_from_required_group id="category-min-tags-from-group" type="number" min="1"}}
|
||||
</section>
|
||||
<section class="field-item required-tag-group">
|
||||
<label>{{i18n "category.required_tag_group_label"}}</label>
|
||||
{{tag-group-chooser
|
||||
id="category-required-tag-group"
|
||||
tagGroups=category.required_tag_groups
|
||||
options=(hash
|
||||
maximum=1
|
||||
filterPlaceholder="category.tag_group_selector_placeholder"
|
||||
)
|
||||
}}
|
||||
<section class="field-item required-tag-groups">
|
||||
{{#each category.required_tag_groups as |rtg|}}
|
||||
<div class="required-tag-group-row">
|
||||
{{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"}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{d-button
|
||||
label="category.required_tag_group.add"
|
||||
action=(action "addRequiredTagGroup")
|
||||
icon="plus"
|
||||
class="add-required-tag-group"}}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -12,17 +12,19 @@
|
||||
<label class="control-label">{{i18n "topic.topic_status_update.publish_to"}}</label>
|
||||
{{category-chooser
|
||||
value=topicTimer.category_id
|
||||
excludeCategoryId=excludeCategoryId
|
||||
onChange=(action (mut topicTimer.category_id))
|
||||
options=(hash
|
||||
excludeCategoryId=excludeCategoryId
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if showFutureDateInput}}
|
||||
<label class="control-label">{{i18n "topic.topic_status_update.when"}}</label>
|
||||
{{time-shortcut-picker
|
||||
timeShortcuts=timeOptions
|
||||
prefilledDatetime=topicTimer.execute_at
|
||||
onTimeSelected=onTimeSelected
|
||||
customOptions=customTimeShortcutOptions
|
||||
hiddenOptions=hiddenTimeShortcutOptions
|
||||
_itsatrap=_itsatrap
|
||||
}}
|
||||
|
||||
@ -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}}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
|
||||
{{#if acceptAnyFile}}
|
||||
<input type="file">
|
||||
{{else}}
|
||||
<input type="file" accept={{acceptedFileTypesString}}>
|
||||
{{#if showButton}}
|
||||
{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
|
||||
{{/if}}
|
||||
{{#if acceptsAllFormats}}
|
||||
<input type="file" id={{fileInputId}} class={{fileInputClass}} multiple={{allowMultiple}} disabled={{fileInputDisabled}}>
|
||||
{{else}}
|
||||
<input type="file" id={{fileInputId}} class={{fileInputClass}} accept={{acceptedFormats}} multiple={{allowMultiple}} disabled={{fileInputDisabled}}>
|
||||
{{/if}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user