Version bump

This commit is contained in:
Penar Musaraj 2022-04-14 10:12:57 -04:00
commit c3a2561121
No known key found for this signature in database
GPG Key ID: E390435D881FF0F7
649 changed files with 11552 additions and 5349 deletions

View File

@ -16,3 +16,4 @@ app/assets/javascripts/discourse/tests/test-boot-rails.js
app/assets/javascripts/discourse/tests/fixtures
node_modules/
dist/
tmp/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
//= link_tree ../images

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
{{category-chooser
value=value
allowUncategorized=true
onChange=(action (mut value))
options=(hash
allowUncategorized=true
none=(eq setting.default "")
)
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ export function getURLWithCDN(url) {
}
export function getAbsoluteURL(path) {
return baseUrl + path;
return baseUrl + withoutPrefix(path);
}
export function isAbsoluteURL(url) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export default Controller.extend(ModalFunctionality, {
this.set("loading", true);
this.model
.updateNotificationLevel({
level: "ignored",
level: "ignore",
expiringAt: this.ignoredUntil,
})
.then(() => {

View File

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

View File

@ -111,7 +111,7 @@ export default Controller.extend({
if (newList.length === 0) {
this.refreshModel();
} else {
this.set("reviewables", newList);
this.reviewables.setObjects(newList);
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -772,7 +772,7 @@ export default {
},
_onScrollEndsCallback() {
document.querySelector(".topic-post.selected a.tabLoc")?.focus();
document.querySelector(".topic-post.selected span.tabLoc")?.focus();
},
categoriesTopicsList() {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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 += "&hellip;";
resultLength += text.length;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) === "&hellip;";
return excerpt && excerpt.slice(-8) === "&hellip;";
},
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,9 +38,9 @@
{{#if userHasTimezoneSet}}
{{time-shortcut-picker
timeShortcuts=timeOptions
prefilledDatetime=prefilledDatetime
onTimeSelected=(action "onTimeSelected")
customOptions=customTimeShortcutOptions
hiddenOptions=hiddenTimeShortcutOptions
customLabels=customTimeShortcutLabels
_itsatrap=_itsatrap

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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