Version bump

This commit is contained in:
Neil Lalonde 2021-02-18 14:24:10 -05:00
commit 33df4233c9
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
905 changed files with 43876 additions and 12675 deletions

View File

@ -12,6 +12,7 @@ jobs:
build:
name: "${{ matrix.target }}-${{ matrix.build_types }}"
runs-on: ${{ matrix.os }}
container: discourse/discourse_test:release
timeout-minutes: 60
env:
@ -20,7 +21,7 @@ jobs:
BUILD_TYPE: ${{ matrix.build_types }}
TARGET: ${{ matrix.target }}
RAILS_ENV: test
PGHOST: localhost
PGHOST: postgres
PGUSER: discourse
PGPASSWORD: discourse
@ -61,49 +62,27 @@ jobs:
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Setup packages
if: env.BUILD_TYPE != 'LINT'
run: |
sudo apt-get update
sudo apt-get -yqq install postgresql-client libpq-dev jpegoptim optipng jhead pngcrush pngquant
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- name: Update imagemagick
if: env.BUILD_TYPE == 'BACKEND'
run: |
wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick
chmod +x install-imagemagick
sudo ./install-imagemagick
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
if: env.BUILD_TYPE != 'LINT'
with:
redis-version: ${{ matrix.redis }}
- name: Setup ruby
uses: actions/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Setup bundler
run: |
gem install bundler -v 2.1.4 --no-doc
bundle config deployment 'true'
bundle config without 'development'
bundle config path vendor/bundle
- name: Bundler cache
uses: actions/cache@v2
id: bundler-cache
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
${{ runner.os }}-${{ matrix.ruby }}-gem-
- name: Setup gems
run: bundle install --jobs 4
run: |
bundle config --local path vendor/bundle
bundle config --local deployment true
bundle config --local without development
bundle install --jobs 4
bundle clean
- name: Get yarn cache directory
id: yarn-cache-dir
@ -119,7 +98,7 @@ jobs:
${{ runner.os }}-${{ matrix.os }}-yarn-
- name: Yarn install
run: yarn install --dev
run: yarn install
- name: "Checkout official plugins"
if: env.TARGET == 'PLUGINS'

7
.gitignore vendored
View File

@ -137,10 +137,8 @@ node_modules
# ignore generated api documentation files
openapi/*
# ignore custom VSCode config files
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
# ignore VSCode config files
.vscode
# ignore direnv
.envrc
@ -152,3 +150,4 @@ dist
copyright
yarn-error.log
tags

View File

@ -40,7 +40,7 @@ gem 'actionview_precompiler', require: false
gem 'seed-fu'
gem 'mail', require: false
gem 'mail', git: 'https://github.com/discourse/mail.git', require: false
gem 'mini_mime'
gem 'mini_suffix'

View File

@ -1,3 +1,10 @@
GIT
remote: https://github.com/discourse/mail.git
revision: 5b700fc95ee66378e0cf2559abc73c8bc3062a4b
specs:
mail (2.8.0.edge)
mini_mime (>= 0.1.1)
GEM
remote: https://rubygems.org/
specs:
@ -72,7 +79,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
bootsnap (1.6.0)
bootsnap (1.7.2)
msgpack (~> 1.0)
builder (3.2.4)
bullet (6.1.3)
@ -93,7 +100,7 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
css_parser (1.8.0)
css_parser (1.9.0)
addressable
debug_inspector (1.0.0)
diff-lcs (1.4.4)
@ -106,7 +113,7 @@ GEM
jquery-rails (>= 1.0.17)
railties (>= 3.1)
discourse-ember-source (3.12.2.2)
discourse-fonts (0.0.5)
discourse-fonts (0.0.6)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
@ -124,7 +131,7 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.10.0)
excon (0.78.1)
excon (0.79.0)
execjs (2.7.0)
exifr (1.3.9)
fabrication (2.21.1)
@ -136,7 +143,7 @@ GEM
faraday-net_http (1.0.1)
fast_blank (1.0.0)
fast_xs (0.8.0)
fastimage (2.2.1)
fastimage (2.2.2)
ffi (1.14.2)
fspath (3.1.2)
gc_tracer (1.5.1)
@ -150,7 +157,7 @@ GEM
hkdf (0.3.0)
htmlentities (4.3.4)
http_accept_language (2.1.1)
i18n (1.8.7)
i18n (1.8.9)
concurrent-ruby (~> 1.0)
image_size (1.5.0)
in_threads (1.5.4)
@ -192,8 +199,6 @@ GEM
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
lz4-ruby (0.3.3)
mail (2.7.1)
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (1.0.0)
message_bus (3.3.4)
@ -217,7 +222,7 @@ GEM
multi_xml (0.6.0)
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.4)
nio4r (2.5.5)
nokogiri (1.11.1)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
@ -236,7 +241,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.11.1)
oj (3.11.2)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -250,9 +255,9 @@ GEM
oauth2 (~> 1.1)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.6)
omniauth-oauth (1.1.0)
omniauth-oauth (1.2.0)
oauth
omniauth (~> 1.0)
omniauth (>= 1.0, < 3)
omniauth-oauth2 (1.7.1)
oauth2 (~> 1.4)
omniauth (>= 1.9, < 3)
@ -266,7 +271,9 @@ GEM
mustache
nokogiri (~> 1.7)
sanitize
openssl-signature_algorithm (1.0.0)
openssl (2.2.0)
openssl-signature_algorithm (1.1.1)
openssl (~> 2.0)
optimist (3.0.1)
parallel (1.20.1)
parallel_tests (3.4.0)
@ -284,12 +291,12 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.2.0)
puma (5.2.1)
nio4r (~> 2.0)
r2 (0.2.7)
racc (1.5.2)
rack (2.2.3)
rack-mini-profiler (2.3.0)
rack-mini-profiler (2.3.1)
rack (>= 1.2.0)
rack-protection (2.1.0)
rack
@ -325,7 +332,7 @@ GEM
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.2.5)
redis-namespace (1.8.0)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.0.3)
request_store (1.5.0)
@ -349,7 +356,7 @@ GEM
rspec-html-matchers (0.9.4)
nokogiri (~> 1)
rspec (>= 3.0.0.a, < 4)
rspec-mocks (3.10.1)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-rails (4.0.2)
@ -360,13 +367,13 @@ GEM
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.1)
rswag-specs (2.3.1)
rspec-support (3.10.2)
rswag-specs (2.4.0)
activesupport (>= 3.1, < 7.0)
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (1.8.1)
rubocop (1.10.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -380,10 +387,10 @@ GEM
rubocop-discourse (2.4.1)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.1.0)
rubocop-rspec (2.2.0)
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
ruby-prof (1.4.2)
ruby-prof (1.4.3)
ruby-progressbar (1.11.0)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
@ -427,7 +434,7 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.16)
test-prof (1.0.0)
test-prof (1.0.1)
thor (1.1.0)
thread_safe (0.3.6)
tilt (2.0.10)
@ -444,7 +451,7 @@ GEM
raindrops (~> 0.7)
uniform_notifier (1.13.2)
uri_template (0.7.0)
webmock (3.11.1)
webmock (3.11.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -515,7 +522,7 @@ DEPENDENCIES
logster
lru_redux
lz4-ruby
mail
mail!
maxminddb
memory_profiler
message_bus

View File

@ -1,9 +1,11 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { action, computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
export default Component.extend({
newFeatures: null,
classNames: ["section", "dashboard-new-features"],
classNameBindings: ["hasUnseenFeatures:ordered-first"],
releaseNotesLink: null,
init() {
@ -12,15 +14,20 @@ export default Component.extend({
ajax("/admin/dashboard/new-features.json").then((json) => {
this.setProperties({
newFeatures: json.new_features,
hasUnseenFeatures: json.has_unseen_features,
releaseNotesLink: json.release_notes_link,
});
});
},
columnCountClass: computed("newFeatures", function () {
return this.newFeatures.length > 2 ? "three-or-more-items" : "";
}),
@action
dismissNewFeatures() {
ajax("/admin/dashboard/mark-new-features-as-seen.json", {
type: "PUT",
}).then(() => this.set("newFeatures", null));
}).then(() => this.set("hasUnseenFeatures", false));
},
});

View File

@ -3,6 +3,7 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { get } from "@ember/object";
import showModal from "discourse/lib/show-modal";
export default Controller.extend({
@ -30,6 +31,10 @@ export default Controller.extend({
},
actions: {
updateUsername(selected) {
this.set("model.username", get(selected, "firstObject"));
},
changeUserMode(value) {
if (value === "all") {
this.model.set("username", null);

View File

@ -231,6 +231,11 @@ export default Controller.extend({
: remoteThemeUrl;
},
@discourseComputed("model.user.id", "model.default")
showConvert(userId, defaultTheme) {
return userId > 0 && !defaultTheme;
},
actions: {
updateToLatest() {
this.set("updatingRemote", true);
@ -369,25 +374,29 @@ export default Controller.extend({
switchType() {
const relatives = this.get("model.component")
? this.parentThemes
? this.get("model.parentThemes")
: this.get("model.childThemes");
let message = I18n.t(`${this.convertKey}_alert_generic`);
if (relatives && relatives.length > 0) {
const names = relatives.map((relative) => relative.get("name"));
bootbox.confirm(
I18n.t(`${this.convertKey}_alert`, {
relatives: names.join(", "),
}),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.commitSwitchType();
}
}
);
} else {
this.commitSwitchType();
message = I18n.t(`${this.convertKey}_alert`, {
relatives: relatives
.map((relative) => relative.get("name"))
.join(", "),
});
}
bootbox.confirm(
message,
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.commitSwitchType();
}
}
);
},
enableComponent() {

View File

@ -2,6 +2,7 @@ import { empty, notEmpty, or } from "@ember/object/computed";
import Controller from "@ember/controller";
import EmailPreview from "admin/models/email-preview";
import bootbox from "bootbox";
import { get } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
@ -14,6 +15,10 @@ export default Controller.extend({
htmlEmpty: empty("model.html_content"),
actions: {
updateUsername(selected) {
this.set("username", get(selected, "firstObject"));
},
refresh() {
const model = this.model;

View File

@ -22,6 +22,7 @@ export default Controller.extend(CanCheckEmails, {
availableGroups: null,
userTitleValue: null,
ssoExternalEmail: null,
ssoLastPayload: null,
showBadges: setting("enable_badges"),
hasLockedTrustLevel: notEmpty("model.manual_locked_trust_level"),
@ -137,7 +138,7 @@ export default Controller.extend(CanCheckEmails, {
.catch(() => bootbox.alert(I18n.t("generic_error")));
},
@discourseComputed("model.single_sign_on_record.last_payload")
@discourseComputed("ssoLastPayload")
ssoPayload(lastPayload) {
return lastPayload.split("&");
},
@ -315,9 +316,8 @@ export default Controller.extend(CanCheckEmails, {
link: true,
},
{
label:
`${iconHTML("exclamation-triangle")} ` +
I18n.t("admin.user.delete_all_posts"),
icon: iconHTML("exclamation-triangle"),
label: I18n.t("admin.user.delete_all_posts"),
class: "btn btn-danger",
callback: () => {
const progressModal = openProgressModal();
@ -367,10 +367,9 @@ export default Controller.extend(CanCheckEmails, {
link: true,
},
{
label:
`${iconHTML("exclamation-triangle")} ` +
I18n.t("admin.user.anonymize_yes"),
label: I18n.t("admin.user.anonymize_yes"),
class: "btn btn-danger",
icon: iconHTML("exclamation-triangle"),
callback: () => {
performAnonymize();
},
@ -435,9 +434,8 @@ export default Controller.extend(CanCheckEmails, {
link: true,
},
{
label:
`${iconHTML("exclamation-triangle")} ` +
I18n.t("admin.user.delete_and_block"),
icon: iconHTML("exclamation-triangle"),
label: I18n.t("admin.user.delete_and_block"),
class: "btn btn-danger",
callback: () => {
performDestroy(true);
@ -593,7 +591,7 @@ export default Controller.extend(CanCheckEmails, {
deleteSSORecord() {
return bootbox.confirm(
I18n.t("admin.user.sso.confirm_delete"),
I18n.t("admin.user.discourse_connect.confirm_delete"),
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
@ -613,5 +611,15 @@ export default Controller.extend(CanCheckEmails, {
}
});
},
checkSsoPayload() {
return ajax(userPath(`${this.model.username_lower}/sso-payload.json`), {
data: { context: window.location.pathname },
}).then((result) => {
if (result) {
this.set("ssoLastPayload", result.payload);
}
});
},
},
});

View File

@ -18,6 +18,7 @@ export default Controller.extend(ModalFunctionality, {
local: equal("selection", "local"),
remote: equal("selection", "remote"),
create: equal("selection", "create"),
directRepoInstall: equal("selection", "directRepoInstall"),
selection: "popular",
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
@ -26,6 +27,7 @@ export default Controller.extend(ModalFunctionality, {
checkPrivate: match("uploadUrl", /^git/),
localFile: null,
uploadUrl: null,
uploadName: null,
advancedVisible: false,
selectedType: alias("themesController.currentTab"),
component: equal("selectedType", COMPONENTS),
@ -136,6 +138,7 @@ export default Controller.extend(ModalFunctionality, {
uploadUrl: null,
publicKey: null,
branch: null,
selection: "popular",
});
},
@ -189,7 +192,7 @@ export default Controller.extend(ModalFunctionality, {
options.data.append("theme", this.localFile);
}
if (this.remote || this.popular) {
if (this.remote || this.popular || this.directRepoInstall) {
const duplicate = this.themesController.model.content.find((theme) =>
this.themeHasSameUrl(theme, this.uploadUrl)
);

View File

@ -22,6 +22,13 @@ export default Controller.extend(ModalFunctionality, {
});
},
@discourseComputed("username")
mergeButtonText(username) {
return I18n.t(`admin.user.merge.confirmation.transfer_and_delete`, {
username,
});
},
@discourseComputed("value", "text")
mergeDisabled(value, text) {
return !value || text !== value;

View File

@ -1,6 +1,7 @@
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { action, get } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
@ -17,6 +18,13 @@ export default Controller.extend(ModalFunctionality, {
return !targetUsername || username === targetUsername;
},
@discourseComputed("username")
mergeButtonText(username) {
return I18n.t(`admin.user.merge.confirmation.transfer_and_delete`, {
username,
});
},
@action
showConfirmation() {
this.send("closeModal");
@ -27,4 +35,9 @@ export default Controller.extend(ModalFunctionality, {
close() {
this.send("closeModal");
},
@action
updateUsername(selected) {
this.set("targetUsername", get(selected, "firstObject"));
},
});

View File

@ -1,8 +1,14 @@
import Route from "@ember/routing/route";
import showModal from "discourse/lib/show-modal";
import { next } from "@ember/runloop";
import { showUnassignedComponentWarning } from "admin/routes/admin-customize-themes-show";
export default Route.extend({
queryParams: {
repoUrl: null,
repoName: null,
},
model() {
return this.store.findAll("theme");
},
@ -10,6 +16,18 @@ export default Route.extend({
setupController(controller, model) {
this._super(controller, model);
controller.set("editingTheme", false);
if (controller.repoUrl) {
next(() => {
showModal("admin-install-theme", {
admin: true,
}).setProperties({
uploadUrl: controller.repoUrl,
uploadName: controller.repoName,
selection: "directRepoInstall",
});
});
}
},
actions: {
@ -30,7 +48,12 @@ export default Route.extend({
addTheme(theme) {
this.refresh();
theme.setProperties({ recentlyInstalled: true });
this.transitionTo("adminCustomizeThemes.show", theme.get("id"));
this.transitionTo("adminCustomizeThemes.show", theme.get("id"), {
queryParams: {
repoName: null,
repoUrl: null,
},
});
},
},
});

View File

@ -7,6 +7,14 @@ export default DiscourseRoute.extend({
filters: { refreshModel: true },
},
beforeModel(transition) {
const params = transition.to.queryParams;
const controller = this.controllerFor("admin-logs-staff-action-logs");
if (controller.filters === null || params.force_refresh) {
controller.resetFilters();
}
},
deserializeQueryParam(value, urlKey, defaultValueType) {
if (urlKey === "filters") {
return EmberObject.create(JSON.parse(decodeURIComponent(value)));
@ -27,13 +35,6 @@ export default DiscourseRoute.extend({
return this._super(value, urlKey, defaultValueType);
},
activate() {
const controller = this.controllerFor("admin-logs-staff-action-logs");
if (controller.filters === null) {
controller.resetFilters();
}
},
// TODO: make this automatic using an `{{outlet}}`
renderTemplate() {
this.render("admin/templates/logs/staff-action-logs", {

View File

@ -25,10 +25,14 @@
{{#if showUserSelector}}
{{#admin-form-row label="admin.api.user"}}
{{user-selector single="true"
usernames=model.username
placeholderKey="admin.api.user_placeholder"
}}
{{email-group-user-chooser
value=model.username
onChange=(action "updateUsername")
options=(hash
maximum=1
filterPlaceholder="admin.api.user_placeholder"
)
}}
{{/admin-form-row}}
{{/if}}

View File

@ -1,21 +1,19 @@
{{#if newFeatures}}
<div class="section dashboard-new-features">
<div class="section-title">
<h2>{{replace-emoji (i18n "admin.dashboard.new_features.title") }}</h2>
</div>
<div class="section-title">
<h2>{{replace-emoji (i18n "admin.dashboard.new_features.title") }}</h2>
</div>
<div class="section-body">
{{#each newFeatures as |feature|}}
{{dashboard-new-feature-item item=feature}}
{{/each}}
</div>
<div class="section-footer">
{{#if releaseNotesLink}}
<a rel="noopener noreferrer" target="_blank" href={{releaseNotesLink}} class="btn btn-primary new-features-release-notes">
{{i18n "admin.dashboard.new_features.learn_more"}}
</a>
{{/if}}
{{d-button label="admin.dashboard.new_features.dismiss" class="new-features-dismiss" action=dismissNewFeatures }}
</div>
<div class="section-body {{columnCountClass}}">
{{#each newFeatures as |feature|}}
{{dashboard-new-feature-item item=feature tagName=""}}
{{/each}}
</div>
<div class="section-footer">
{{#if releaseNotesLink}}
<a rel="noopener noreferrer" target="_blank" href={{releaseNotesLink}} class="btn btn-primary new-features-release-notes">
{{i18n "admin.dashboard.new_features.learn_more"}}
</a>
{{/if}}
{{d-button label="admin.dashboard.new_features.dismiss" class="new-features-dismiss" action=dismissNewFeatures }}
</div>
{{/if}}

View File

@ -13,8 +13,6 @@
<div class="admin-footer">
<div class="buttons">
{{#d-button action=(action "reset") disabled=resetDisabled class="btn-default"}}
{{i18n "admin.customize.email_style.reset"}}
{{/d-button}}
{{d-button action=(action "reset") disabled=resetDisabled class="btn-default" label="admin.customize.email_style.reset"}}
</div>
</div>

View File

@ -2,7 +2,7 @@
<h3>
{{#if staffLogFilter}}
{{settingName}}
{{#link-to "adminLogs.staffActionLogs" (query-params filters=staffLogFilter) title=(i18n "admin.settings.history")}}
{{#link-to "adminLogs.staffActionLogs" (query-params filters=staffLogFilter force_refresh=true) title=(i18n "admin.settings.history")}}
<span class="history-icon">
{{d-icon "history"}}
</span>

View File

@ -1,6 +1,7 @@
{{tag-chooser
tags=selectedTags
onChange=(action "changeSelectedTags")
everyTag=true
options=(hash
allowAny=false
)

View File

@ -2,8 +2,6 @@
<div class="admin-footer">
<div class="buttons">
{{#d-button action=(action "save") disabled=saveDisabled class="btn-primary"}}
{{saveButtonText}}
{{/d-button}}
{{d-button action=(action "save") disabled=saveDisabled class="btn-primary" translatedLabel=saveButtonText}}
</div>
</div>

View File

@ -43,13 +43,12 @@
</div>
<div class="buttons">
{{#d-button
{{d-button
action=(action "save")
disabled=saveDisabled
class="btn-primary"
translatedLabel=saveButtonText
}}
{{saveButtonText}}
{{/d-button}}
</div>
</div>
</div>

View File

@ -84,6 +84,9 @@
<div class="control-unit">
{{#if model.remote_theme.is_git}}
<div class="alert alert-info">
{{html-safe (i18n "admin.customize.theme.remote_theme_edits" repoURL=remoteThemeLink)}}
</div>
{{#if showRemoteError}}
<div class="error-message">
@ -290,9 +293,9 @@
<a href={{previewUrl}} title={{i18n "admin.customize.explain_preview"}} rel="noopener noreferrer" target="_blank" class="btn btn-default">{{d-icon "desktop"}}{{i18n "admin.customize.theme.preview"}}</a>
<a class="btn btn-default export" rel="noopener noreferrer" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n "admin.export_json.button_text"}}</a>
{{#unless model.default}}
{{#if showConvert}}
{{d-button action=(action "switchType") label="admin.customize.theme.convert" icon=convertIcon class="btn-default btn-normal" title=convertTooltip}}
{{/unless}}
{{/if}}
{{#if model.component}}
{{#if model.enabled}}

View File

@ -1,5 +1,3 @@
{{dashboard-new-features}}
{{plugin-outlet name="admin-dashboard-top"}}
{{#if showVersionChecks}}
@ -52,4 +50,6 @@
{{outlet}}
{{dashboard-new-features tagName="div"}}
{{plugin-outlet name="admin-dashboard-bottom"}}

View File

@ -5,7 +5,13 @@
<label for="last-seen">{{i18n "admin.email.last_seen_user"}}</label>
{{date-picker-past value=lastSeen id="last-seen"}}
<label>{{i18n "admin.email.user"}}:</label>
{{user-selector single="true" usernames=username canReceiveUpdates=true}}
{{email-group-user-chooser
value=username
onChange=(action "updateUsername")
options=(hash
maximum=1
)
}}
{{d-button
class="btn-primary digest-refresh-button"
action=(action "refresh")

View File

@ -1,10 +1,12 @@
{{#d-modal-body class="upload-selector install-theme" title="admin.customize.theme.install"}}
<div class="install-theme-items">
{{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}}
{{install-theme-item value="local" selection=selection label="admin.customize.theme.install_upload"}}
{{install-theme-item value="remote" selection=selection label="admin.customize.theme.install_git_repo"}}
{{install-theme-item value="create" selection=selection label="admin.customize.theme.install_create" showIcon=true}}
</div>
{{#unless directRepoInstall}}
<div class="install-theme-items">
{{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}}
{{install-theme-item value="local" selection=selection label="admin.customize.theme.install_upload"}}
{{install-theme-item value="remote" selection=selection label="admin.customize.theme.install_git_repo"}}
{{install-theme-item value="create" selection=selection label="admin.customize.theme.install_create" showIcon=true}}
</div>
{{/unless}}
<div class="install-theme-content">
{{#if popular}}
<div class="popular-theme-items">
@ -97,6 +99,13 @@
}}
</div>
{{/if}}
{{#if directRepoInstall}}
<div class="repo">
<div class="label">{{html-safe (i18n "admin.customize.theme.direct_install_tip" name=uploadName)}}</div>
<pre><code>{{uploadUrl}}</code></pre>
</div>
{{/if}}
</div>
{{/d-modal-body}}

View File

@ -5,14 +5,13 @@
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button
{{d-button
class="btn-danger"
action=(action "confirm")
icon="trash-alt"
disabled=mergeDisabled
translatedLabel=mergeButtonText
}}
{{i18n "admin.user.merge.confirmation.transfer_and_delete" username=username}}
{{/d-button}}
{{d-button
action=(action "close")
label="admin.user.merge.confirmation.cancel"

View File

@ -1,21 +1,24 @@
<div>
{{#d-modal-body rawTitle=(i18n "admin.user.merge.prompt.title" username=username)}}
<p>{{html-safe (i18n "admin.user.merge.prompt.description" username=username)}}</p>
{{user-selector single=true
placeholderKey="admin.user.merge.prompt.target_username_placeholder"
usernames=targetUsername
autocomplete="discourse"}}
{{email-group-user-chooser
value=targetUsername
onChange=(action "updateUsername")
options=(hash
maximum=1
filterPlaceholder="admin.user.merge.prompt.target_username_placeholder"
)
}}
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button
{{d-button
class="btn-primary"
action=(action "showConfirmation")
icon="trash-alt"
disabled=mergeDisabled
translatedLabel=mergeButtonText
}}
{{i18n "admin.user.merge.prompt.transfer_and_delete" username=username}}
{{/d-button}}
{{d-button
action=(action "close")
label="admin.user.merge.prompt.cancel"

View File

@ -23,11 +23,16 @@
</td>
<td class="plugin-name">
{{#if plugin.url}}
<a href={{plugin.url}} rel="noopener noreferrer" target="_blank">{{plugin.name}}</a>
{{else}}
{{plugin.name}}
{{/if}}
<div class="name">
{{#if plugin.url}}
<a href={{plugin.url}} rel="noopener noreferrer" target="_blank">{{plugin.name}}</a>
{{else}}
{{plugin.name}}
{{/if}}
</div>
<div class="about">
{{plugin.about}}
</div>
</td>
<td class="version">
<div class="label">{{i18n "admin.plugins.version"}}</div>

View File

@ -21,4 +21,4 @@
label="admin.user_fields.create"
icon="plus"}}
</div>
</div>

View File

@ -70,8 +70,8 @@
{{/if}}
</div>
<div class="controls">
{{#if siteSettings.sso_overrides_email}}
{{i18n "user.email.sso_override_instructions"}}
{{#if siteSettings.auth_overrides_email}}
{{i18n "user.email.auth_override_instructions"}}
{{else if model.email}}
{{html-safe (i18n "admin.user.visit_profile" url=preferencesPath)}}
{{/if}}
@ -105,8 +105,8 @@
<div class="controls">
{{#if model.email}}
{{#if model.secondary_emails}}
{{#if siteSettings.sso_overrides_email}}
{{i18n "user.email.sso_override_instructions"}}
{{#if siteSettings.auth_overrides_email}}
{{i18n "user.email.auth_override_instructions"}}
{{else}}
{{html-safe (i18n "admin.user.visit_profile" url=preferencesPath)}}
{{/if}}
@ -646,11 +646,11 @@
{{#if model.single_sign_on_record}}
<section class="details">
<h1>{{i18n "admin.user.sso.title"}}</h1>
<h1>{{i18n "admin.user.discourse_connect.title"}}</h1>
{{#with model.single_sign_on_record as |sso|}}
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.external_id"}}</div>
<div class="field">{{i18n "admin.user.discourse_connect.external_id"}}</div>
<div class="value">{{sso.external_id}}</div>
{{#if model.can_delete_sso_record}}
<div class="controls">
@ -658,22 +658,22 @@
class="btn-danger"
action=(action "deleteSSORecord")
icon="far-trash-alt"
label="admin.user.sso.delete_sso_record"
label="admin.user.discourse_connect.delete_sso_record"
}}
</div>
{{/if}}
</div>
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.external_username"}}</div>
<div class="field">{{i18n "admin.user.discourse_connect.external_username"}}</div>
<div class="value">{{sso.external_username}}</div>
</div>
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.external_name"}}</div>
<div class="field">{{i18n "admin.user.discourse_connect.external_name"}}</div>
<div class="value">{{sso.external_name}}</div>
</div>
{{#if canAdminCheckEmails}}
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.external_email"}}</div>
<div class="field">{{i18n "admin.user.discourse_connect.external_email"}}</div>
{{#if ssoExternalEmail}}
<div class="value">{{ssoExternalEmail}}</div>
{{else}}
@ -687,17 +687,26 @@
</div>
{{/if}}
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.external_avatar_url"}}</div>
<div class="field">{{i18n "admin.user.discourse_connect.external_avatar_url"}}</div>
<div class="value">{{sso.external_avatar_url}}</div>
</div>
{{#if sso.last_payload}}
{{#if canAdminCheckEmails}}
<div class="display-row">
<div class="field">{{i18n "admin.user.sso.last_payload"}}</div>
<div class="value">
{{#each ssoPayload as |line|}}
{{line}}<br>
{{/each}}
</div>
<div class="field">{{i18n "admin.user.discourse_connect.last_payload"}}</div>
{{#if ssoLastPayload}}
<div class="value">
{{#each ssoPayload as |line|}}
{{line}}<br>
{{/each}}
</div>
{{else}}
{{d-button
class="btn-default"
action=(action "checkSsoPayload")
actionParam=model icon="far-list-alt"
label="admin.users.check_sso.text"
title="admin.users.check_sso.title"}}
{{/if}}
</div>
{{/if}}
{{/with}}

View File

@ -1,4 +1,4 @@
import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers";
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
@ -18,13 +18,14 @@ acceptance("Admin - Themes - Install modal", function (needs) {
await click(".install-theme-content .inputs .advanced-repo");
await fillIn(branchInput, "tests-passed");
await click(privateRepoCheckbox);
assert.ok(queryAll(urlInput)[0].value === themeUrl, "url input is filled");
assert.ok(
queryAll(branchInput)[0].value === "tests-passed",
assert.equal(query(urlInput).value, themeUrl, "url input is filled");
assert.equal(
query(branchInput).value,
"tests-passed",
"branch input is filled"
);
assert.ok(
queryAll(privateRepoCheckbox)[0].checked,
query(privateRepoCheckbox).checked,
"private repo checkbox is checked"
);
@ -32,11 +33,21 @@ acceptance("Admin - Themes - Install modal", function (needs) {
await click(".create-actions .btn-primary");
await click("#remote");
assert.ok(queryAll(urlInput)[0].value === "", "url input is reset");
assert.ok(queryAll(branchInput)[0].value === "", "branch input is reset");
assert.equal(query(urlInput).value, "", "url input is reset");
assert.equal(query(branchInput).value, "", "branch input is reset");
assert.ok(
!queryAll(privateRepoCheckbox)[0].checked,
!query(privateRepoCheckbox).checked,
"private repo checkbox unchecked"
);
});
test("modal can be auto-opened with the right query params", async function (assert) {
await visit("/admin/customize/themes?repoUrl=testUrl&repoName=testName");
assert.ok(query(".admin-install-theme-modal"), "modal is visible");
assert.equal(
query(".install-theme code").textContent.trim(),
"testUrl",
"repo url is visible"
);
});
});

View File

@ -112,16 +112,31 @@ var define, requirejs;
isPresent: Ember.isPresent,
},
rsvp: {
asap: Ember.RSVP.asap,
all: Ember.RSVP.all,
allSettled: Ember.RSVP.allSettled,
race: Ember.RSVP.race,
hash: Ember.RSVP.hash,
hashSettled: Ember.RSVP.hashSettled,
rethrow: Ember.RSVP.rethrow,
defer: Ember.RSVP.defer,
denodeify: Ember.RSVP.denodeify,
resolve: Ember.RSVP.resolve,
reject: Ember.RSVP.reject,
map: Ember.RSVP.map,
filter: Ember.RSVP.filter,
default: Ember.RSVP,
Promise: Ember.RSVP.Promise,
hash: Ember.RSVP.hash,
all: Ember.RSVP.all,
EventTarget: Ember.RSVP.EventTarget,
},
"@ember/string": {
w: Ember.String.w,
dasherize: Ember.String.dasherize,
decamelize: Ember.String.decamelize,
camelize: Ember.String.camelize,
classify: Ember.String.classify,
underscore: Ember.String.underscore,
camelize: Ember.String.camelize,
capitalize: Ember.String.capitalize,
},
"@ember/template": {
htmlSafe: Ember.String.htmlSafe,

View File

@ -1,57 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import { action } from "@ember/object";
import { getOwner } from "discourse-common/lib/get-owner";
import { or } from "@ember/object/computed";
export default Component.extend({
tagName: "",
init() {
this._super(...arguments);
this.loadLocalDates();
},
get postLocalDateFormatted() {
return this.postLocalDate().format(I18n.t("dates.long_no_year"));
},
showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"),
loadLocalDates() {
let postEl = document.querySelector(`[data-post-id="${this.postId}"]`);
let localDateEl = null;
if (postEl) {
localDateEl = postEl.querySelector(".discourse-local-date");
}
this.setProperties({
postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null,
postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null,
postDetectedLocalTimezone: localDateEl
? localDateEl.dataset.timezone
: null,
});
},
postLocalDate() {
const bookmarkController = getOwner(this).lookup("controller:bookmark");
let parsedPostLocalDate = bookmarkController._parseCustomDateTime(
this.postDetectedLocalDate,
this.postDetectedLocalTime,
this.postDetectedLocalTimezone
);
if (!this.postDetectedLocalTime) {
return bookmarkController.startOfDay(parsedPostLocalDate);
}
return parsedPostLocalDate;
},
@action
setReminder() {
return this.onChange(this.postLocalDate());
},
});

View File

@ -0,0 +1,422 @@
import {
LATER_TODAY_CUTOFF_HOUR,
MOMENT_THURSDAY,
laterToday,
now,
parseCustomDatetime,
startOfDay,
tomorrow,
} from "discourse/lib/time-utils";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import Component from "@ember/component";
import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import { Promise } from "rsvp";
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import { formattedReminderTime } from "discourse/lib/bookmark";
import { and, notEmpty, or } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { later } from "@ember/runloop";
// global shortcuts that interfere with these modal shortcuts, they are rebound when the
// modal is closed
//
// d deletePost
const GLOBAL_SHORTCUTS_TO_PAUSE = ["d"];
const BOOKMARK_BINDINGS = {
enter: { handler: "saveAndClose" },
"d d": { handler: "delete" },
};
export default Component.extend({
tagName: "",
errorMessage: null,
selectedReminderType: null,
_closeWithoutSaving: null,
_savingBookmarkManually: null,
_saving: null,
_deleting: null,
postDetectedLocalDate: null,
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
prefilledDatetime: null,
userTimezone: null,
showOptions: null,
model: null,
afterSave: null,
@on("init")
_setup() {
this.setProperties({
errorMessage: null,
selectedReminderType: TIME_SHORTCUT_TYPES.NONE,
_closeWithoutSaving: false,
_savingBookmarkManually: false,
_saving: false,
_deleting: false,
postDetectedLocalDate: null,
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
prefilledDatetime: null,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
showOptions: false,
});
this.registerOnCloseHandler(this._onModalClose.bind(this));
this._loadBookmarkOptions();
this._bindKeyboardShortcuts();
if (this.editingExistingBookmark) {
this._initializeExistingBookmarkData();
}
this._loadPostLocalDates();
},
@on("didInsertElement")
_prepareUI() {
later(() => {
if (this.site.isMobileDevice) {
document.getElementById("bookmark-name").blur();
}
});
// we want to make sure the options panel opens so the user
// knows they have set these options previously.
if (this.autoDeletePreference) {
this.toggleOptionsPanel();
}
},
_initializeExistingBookmarkData() {
if (this.existingBookmarkHasReminder) {
this.set("prefilledDatetime", this.model.reminderAt);
let parsedDatetime = parseCustomDatetime(
this.prefilledDatetime,
null,
this.userTimezone
);
this.set("selectedDatetime", parsedDatetime);
}
},
_loadBookmarkOptions() {
this.set(
"autoDeletePreference",
this.model.autoDeletePreference || this._preferredDeleteOption() || 0
);
},
_preferredDeleteOption() {
let preferred = localStorage.bookmarkDeleteOption;
if (preferred && preferred !== "") {
preferred = parseInt(preferred, 10);
}
return preferred;
},
_bindKeyboardShortcuts() {
KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE);
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
KeyboardShortcuts.addShortcut(shortcut, () => {
let binding = BOOKMARK_BINDINGS[shortcut];
if (binding.args) {
return this.send(binding.handler, ...binding.args);
}
this.send(binding.handler);
});
});
},
_unbindKeyboardShortcuts() {
KeyboardShortcuts.unbind(BOOKMARK_BINDINGS);
},
_restoreGlobalShortcuts() {
KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE);
},
_loadPostLocalDates() {
let postEl = document.querySelector(
`[data-post-id="${this.model.postId}"]`
);
let localDateEl;
if (postEl) {
localDateEl = postEl.querySelector(".discourse-local-date");
}
if (localDateEl) {
this.setProperties({
postDetectedLocalDate: localDateEl.dataset.date,
postDetectedLocalTime: localDateEl.dataset.time,
postDetectedLocalTimezone: localDateEl.dataset.timezone,
});
}
},
_saveBookmark() {
let reminderAt;
if (this.selectedReminderType) {
reminderAt = this.selectedDatetime;
}
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
if (this.selectedReminderType === TIME_SHORTCUT_TYPES.CUSTOM) {
if (!reminderAt) {
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
}
}
localStorage.bookmarkDeleteOption = this.autoDeletePreference;
let reminderType;
if (this.selectedReminderType === TIME_SHORTCUT_TYPES.NONE) {
reminderType = null;
} else if (
this.selectedReminderType === TIME_SHORTCUT_TYPES.LAST_CUSTOM ||
this.selectedReminderType === TIME_SHORTCUT_TYPES.POST_LOCAL_DATE
) {
reminderType = TIME_SHORTCUT_TYPES.CUSTOM;
} else {
reminderType = this.selectedReminderType;
}
const data = {
reminder_type: reminderType,
reminder_at: reminderAtISO,
name: this.model.name,
post_id: this.model.postId,
id: this.model.id,
auto_delete_preference: this.autoDeletePreference,
};
if (this.editingExistingBookmark) {
return ajax(`/bookmarks/${this.model.id}`, {
type: "PUT",
data,
}).then((response) => {
this._executeAfterSave(response, reminderAtISO);
});
} else {
return ajax("/bookmarks", { type: "POST", data }).then((response) => {
this._executeAfterSave(response, reminderAtISO);
});
}
},
_executeAfterSave(response, reminderAtISO) {
if (!this.afterSave) {
return;
}
this.afterSave({
reminderAt: reminderAtISO,
reminderType: this.selectedReminderType,
autoDeletePreference: this.autoDeletePreference,
id: this.model.id || response.id,
name: this.model.name,
});
},
_deleteBookmark() {
return ajax("/bookmarks/" + this.model.id, {
type: "DELETE",
}).then((response) => {
if (this.afterDelete) {
this.afterDelete(response.topic_bookmarked);
}
});
},
_postLocalDate() {
let parsedPostLocalDate = parseCustomDatetime(
this.postDetectedLocalDate,
this.postDetectedLocalTime,
this.userTimezone,
this.postDetectedLocalTimezone
);
if (!this.postDetectedLocalTime) {
return startOfDay(parsedPostLocalDate);
}
return parsedPostLocalDate;
},
_handleSaveError(e) {
this._savingBookmarkManually = false;
if (typeof e === "string") {
bootbox.alert(e);
} else {
popupAjaxError(e);
}
},
_onModalClose(initiatedByCloseButton) {
// we want to close without saving if the user already saved
// manually or deleted the bookmark, as well as when the modal
// is just closed with the X button
this._closeWithoutSaving =
this._closeWithoutSaving || initiatedByCloseButton;
this._unbindKeyboardShortcuts();
this._restoreGlobalShortcuts();
if (!this._closeWithoutSaving && !this._savingBookmarkManually) {
this._saveBookmark().catch((e) => this._handleSaveError(e));
}
if (this.onCloseWithoutSaving && this._closeWithoutSaving) {
this.onCloseWithoutSaving();
}
},
showExistingReminderAt: notEmpty("model.reminderAt"),
showDelete: notEmpty("model.id"),
userHasTimezoneSet: notEmpty("userTimezone"),
showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"),
editingExistingBookmark: and("model", "model.id"),
existingBookmarkHasReminder: and("model", "model.reminderAt"),
@discourseComputed()
autoDeletePreferences: () => {
return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => {
return {
id: AUTO_DELETE_PREFERENCES[key],
name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`),
};
});
},
@discourseComputed()
customTimeShortcutOptions() {
let customOptions = [];
if (this.showPostLocalDate) {
customOptions.push({
icon: "globe-americas",
id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE,
label: "bookmarks.reminders.post_local_date",
time: this._postLocalDate(),
timeFormatted: this._postLocalDate().format(
I18n.t("dates.long_no_year")
),
hidden: false,
});
}
return customOptions;
},
@discourseComputed()
additionalTimeShortcutOptions() {
let additional = [];
if (
!laterToday(this.userTimezone).isSame(
tomorrow(this.userTimezone),
"date"
) &&
now(this.userTimezone).hour() < LATER_TODAY_CUTOFF_HOUR
) {
additional.push(TIME_SHORTCUT_TYPES.LATER_TODAY);
}
if (now(this.userTimezone).day() < MOMENT_THURSDAY) {
additional.push(TIME_SHORTCUT_TYPES.LATER_THIS_WEEK);
}
return additional;
},
@discourseComputed("model.reminderAt")
existingReminderAtFormatted(existingReminderAt) {
return formattedReminderTime(existingReminderAt, this.userTimezone);
},
@action
saveAndClose() {
if (this._saving || this._deleting) {
return;
}
this._saving = true;
this._savingBookmarkManually = true;
return this._saveBookmark()
.then(() => this.closeModal())
.catch((e) => this._handleSaveError(e))
.finally(() => (this._saving = false));
},
@action
toggleOptionsPanel() {
if (this.showOptions) {
$(".bookmark-options-panel").slideUp("fast");
} else {
$(".bookmark-options-panel").slideDown("fast");
}
this.toggleProperty("showOptions");
},
@action
delete() {
this._deleting = true;
let deleteAction = () => {
this._closeWithoutSaving = true;
this._deleteBookmark()
.then(() => {
this._deleting = false;
this.closeModal();
})
.catch((e) => this._handleSaveError(e));
};
if (this.existingBookmarkHasReminder) {
bootbox.confirm(I18n.t("bookmarks.confirm_delete"), (result) => {
if (result) {
deleteAction();
}
});
} else {
deleteAction();
}
},
@action
closeWithoutSavingBookmark() {
this._closeWithoutSaving = true;
this.closeModal();
},
@action
onTimeSelected(type, time) {
this.setProperties({ selectedReminderType: type, selectedDatetime: time });
// if the type is custom, we need to wait for the user to click save, as
// they could still be adjusting the date and time
if (
![TIME_SHORTCUT_TYPES.CUSTOM, TIME_SHORTCUT_TYPES.RELATIVE].includes(type)
) {
return this.saveAndClose();
}
},
@action
selectPostLocalDate(date) {
this.setProperties({
selectedReminderType: this.reminderTypes.POST_LOCAL_DATE,
postLocalDate: date,
});
return this.saveAndClose();
},
});

View File

@ -9,13 +9,11 @@ export default Component.extend({
this._super(...arguments);
schedule("afterRender", () => {
let mainOutletPadding =
window.getComputedStyle(document.querySelector("#main-outlet"))
.paddingTop || 0;
let headerHeight =
document.querySelector(".d-header-wrap").offsetHeight || 0;
document.querySelector(
".bulk-select-container"
).style.top = mainOutletPadding;
document.querySelector(".bulk-select-container").style.top =
headerHeight + 20 + "px";
});
},

View File

@ -95,6 +95,9 @@ export default Component.extend({
.mapBy("topic")
.filter((t) => t.id !== currentTopicId)
);
if (this.topics.length === 1) {
this.send("chooseTopic", this.topics[0]);
}
} else {
this.setProperties({ topics: null, loading: false });
}
@ -110,6 +113,7 @@ export default Component.extend({
this.set("selectedTopicId", topic.id);
next(() => {
document.getElementById(`choose-topic-${topic.id}`).checked = true;
document.getElementById(`choose-topic-${topic.id}`).focus();
});
if (this.topicChangedCallback) {
this.topicChangedCallback(topic);

View File

@ -239,7 +239,9 @@ export default Component.extend({
if (replyLength < 1) {
reason = I18n.t("composer.error.post_missing");
} else if (missingReplyCharacters > 0) {
reason = I18n.t("composer.error.post_length", { min: minimumPostLength });
reason = I18n.t("composer.error.post_length", {
count: minimumPostLength,
});
const tl = this.get("currentUser.trust_level");
if (tl === 0 || tl === 1) {
reason +=

View File

@ -44,11 +44,11 @@ export default Component.extend({
reason = I18n.t("composer.error.title_missing");
} else if (missingTitleChars > 0) {
reason = I18n.t("composer.error.title_too_short", {
min: minimumTitleLength,
count: minimumTitleLength,
});
} else if (titleLength > this.siteSettings.max_topic_title_length) {
reason = I18n.t("composer.error.title_too_long", {
max: this.siteSettings.max_topic_title_length,
count: this.siteSettings.max_topic_title_length,
});
}

View File

@ -1,13 +1,12 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
import { schedule } from "@ember/runloop";
export default Component.extend({
showSelector: true,
shouldHide: false,
defaultUsernameCount: 0,
init() {
this._super(...arguments);
this.set("_groups", []);
},
didInsertElement() {
this._super(...arguments);
@ -17,78 +16,34 @@ export default Component.extend({
}
},
@observes("usernames")
_checkWidth() {
let width = 0;
const $acWrap = $(this.element).find(".ac-wrap");
const limit = $acWrap.width();
this.set("defaultUsernameCount", 0);
$acWrap
.find(".item")
.toArray()
.forEach((item) => {
width += $(item).outerWidth(true);
const result = width < limit;
if (result) {
this.incrementProperty("defaultUsernameCount");
}
return result;
});
if (width >= limit) {
this.set("shouldHide", true);
} else {
this.set("shouldHide", false);
}
@discourseComputed("recipients")
splitRecipients(recipients) {
return recipients ? recipients.split(",").filter(Boolean) : [];
},
@observes("shouldHide")
_setFocus() {
const selector =
"#reply-control #reply-title, #reply-control .d-editor-input";
if (this.shouldHide) {
$(selector).on("focus.composer-user-selector", () => {
this.set("showSelector", false);
this.appEvents.trigger("composer:resize");
});
} else {
$(selector).off("focus.composer-user-selector");
}
},
@discourseComputed("usernames")
splitUsernames(usernames) {
return usernames.split(",");
},
@discourseComputed("splitUsernames", "defaultUsernameCount")
limitedUsernames(splitUsernames, count) {
return splitUsernames.slice(0, count).join(", ");
},
@discourseComputed("splitUsernames", "defaultUsernameCount")
hiddenUsersCount(splitUsernames, count) {
return `${splitUsernames.length - count} ${I18n.t("more")}`;
_updateGroups(selected, newGroups) {
const groups = [];
this._groups.forEach((existing) => {
if (selected.includes(existing)) {
groups.addObject(existing);
}
});
newGroups.forEach((newGroup) => {
if (!groups.includes(newGroup)) {
groups.addObject(newGroup);
}
});
this.setProperties({
_groups: groups,
hasGroups: groups.length > 0,
});
},
actions: {
toggleSelector() {
this.set("showSelector", true);
schedule("afterRender", () => {
$(this.element).find("input").focus();
});
},
triggerResize() {
this.appEvents.trigger("composer:resize");
const $this = $(this.element).find(".ac-wrap");
if ($this.height() >= 150) {
$this.scrollTop($this.height());
}
updateRecipients(selected, content) {
const newGroups = content.filterBy("isGroup").mapBy("id");
this._updateGroups(selected, newGroups);
this.set("recipients", selected.join(","));
},
},
});

View File

@ -4,6 +4,23 @@ import cookie from "discourse/lib/cookie";
export default Component.extend({
classNames: ["create-account"],
userInputFocus(event) {
let label = event.target.parentElement.previousElementSibling;
if (!label.classList.contains("value-entered")) {
label.classList.toggle("value-entered");
}
},
userInputFocusOut(event) {
let label = event.target.parentElement.previousElementSibling;
if (
event.target.value.length === 0 &&
label.classList.contains("value-entered")
) {
label.classList.toggle("value-entered");
}
},
didInsertElement() {
this._super(...arguments);
@ -11,6 +28,21 @@ export default Component.extend({
this.set("email", cookie("email"));
}
let userTextFields = document.getElementsByClassName("user-fields")[0];
if (userTextFields) {
userTextFields = userTextFields.getElementsByClassName(
"ember-text-field"
);
}
if (userTextFields) {
for (let element of userTextFields) {
element.addEventListener("focus", this.userInputFocus);
element.addEventListener("focusout", this.userInputFocusOut);
}
}
$(this.element).on("keydown.discourse-create-account", (e) => {
if (!this.disabled && e.keyCode === 13) {
e.preventDefault();
@ -36,5 +68,20 @@ export default Component.extend({
$(this.element).off("keydown.discourse-create-account");
$(this.element).off("click.dropdown-user-field-label");
let userTextFields = document.getElementsByClassName("user-fields")[0];
if (userTextFields) {
userTextFields = userTextFields.getElementsByClassName(
"ember-text-field"
);
}
if (userTextFields) {
for (let element of userTextFields) {
element.removeEventListener("focus", this.userInputFocus);
element.removeEventListener("focusout", this.userInputFocusOut);
}
}
},
});

View File

@ -16,6 +16,8 @@ export default Component.extend({
label: null,
translatedLabel: null,
ariaLabel: null,
ariaExpanded: null,
ariaControls: null,
translatedAriaLabel: null,
forwardEvent: false,
@ -38,6 +40,8 @@ export default Component.extend({
"isDisabled:disabled",
"computedTitle:title",
"computedAriaLabel:aria-label",
"computedAriaExpanded:aria-expanded",
"ariaControls:aria-controls",
"tabindex",
"type",
],
@ -90,6 +94,16 @@ export default Component.extend({
return computedLabel;
},
@discourseComputed("ariaExpanded")
computedAriaExpanded(ariaExpanded) {
if (ariaExpanded === true) {
return "true";
}
if (ariaExpanded === false) {
return "false";
}
},
click(event) {
let { action } = this;

View File

@ -24,6 +24,11 @@ import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { getRegister } from "discourse-common/lib/get-owner";
import { isEmpty } from "@ember/utils";
import { isTesting } from "discourse-common/config/environment";
import { linkSeenHashtags } from "discourse/lib/link-hashtags";
import { linkSeenMentions } from "discourse/lib/link-mentions";
import { loadOneboxes } from "discourse/lib/load-oneboxes";
import loadScript from "discourse/lib/load-script";
import { resolveCachedShortUrls } from "pretty-text/upload-short-url";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal";
@ -389,6 +394,32 @@ export default Component.extend({
}
this.set("preview", cooked);
if (this.siteSettings.enable_diffhtml_preview) {
const cookedElement = document.createElement("div");
cookedElement.innerHTML = cooked;
linkSeenHashtags($(cookedElement));
linkSeenMentions($(cookedElement), this.siteSettings);
resolveCachedShortUrls(this.siteSettings, cookedElement);
loadOneboxes(
cookedElement,
null,
null,
null,
this.siteSettings.max_oneboxes_per_post,
false,
true
);
loadScript("/javascripts/diffhtml.min.js").then(() => {
window.diff.innerHTML(
this.element.querySelector(".d-editor-preview"),
cookedElement.innerHTML
);
});
}
schedule("afterRender", () => {
if (this._state !== "inDOM") {
return;
@ -542,7 +573,10 @@ export default Component.extend({
}
}
const options = emojiSearch(term, { maxResults: 5 });
const options = emojiSearch(term, {
maxResults: 5,
diversity: this.emojiStore.diversity,
});
return resolve(options);
})

View File

@ -62,7 +62,11 @@ export default Component.extend({
},
_clearFlash() {
$("#modal-alert").hide().removeClass("alert-error", "alert-success");
const modalAlert = document.getElementById("modal-alert");
if (modalAlert) {
modalAlert.style.display = "none";
modalAlert.classList.remove("alert-info", "alert-error", "alert-success");
}
},
_flash(msg) {

View File

@ -0,0 +1,17 @@
import Component from "@ember/component";
import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "li",
route: null,
router: service(),
attributeBindings: ["ariaCurrent:aria-current", "title"],
ariaCurrent: computed("router.currentRouteName", "route", function () {
return this.router.currentRouteName === this.route ? "page" : null;
}),
});

View File

@ -22,6 +22,12 @@ export default Component.extend(FilterModeMixin, {
return category && this.currentUser;
},
// don't show tag notification menu on tag intersections
@discourseComputed("tagNotification", "additionalTags")
showTagNotifications(tagNotification, additionalTags) {
return tagNotification && !additionalTags;
},
@discourseComputed("category", "createTopicDisabled")
categoryReadOnlyBanner(category, createTopicDisabled) {
if (category && this.currentUser && createTopicDisabled) {

View File

@ -5,6 +5,7 @@ import { SEARCH_PRIORITIES } from "discourse/lib/constants";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import discourseComputed from "discourse-common/utils/decorators";
import { setting } from "discourse/lib/computed";
import { action } from "@ember/object";
const categorySortCriteria = [];
export function addCategorySortCriteria(criteria) {
@ -126,4 +127,15 @@ export default buildCategoryPanel("settings", {
{ name: I18n.t("category.sort_descending"), value: false },
];
},
@discourseComputed
hiddenRelativeIntervals() {
return ["mins"];
},
@action
onAutoCloseDurationChange(minutes) {
let hours = minutes / 60;
this.set("category.auto_close_hours", hours);
},
});

View File

@ -1,31 +1,53 @@
import {
BUMP_TYPE,
CLOSE_AFTER_LAST_POST_STATUS_TYPE,
CLOSE_STATUS_TYPE,
DELETE_REPLIES_TYPE,
DELETE_STATUS_TYPE,
OPEN_STATUS_TYPE,
PUBLISH_TO_CATEGORY_STATUS_TYPE,
} from "discourse/controllers/edit-topic-timer";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import { equal, or, readOnly } from "@ember/object/computed";
import I18n from "I18n";
import { action } from "@ember/object";
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { isEmpty } from "@ember/utils";
import { now, startOfDay, thisWeekend } from "discourse/lib/time-utils";
export default Component.extend({
selection: readOnly("topicTimer.status_type"),
autoOpen: equal("selection", OPEN_STATUS_TYPE),
autoClose: equal("selection", CLOSE_STATUS_TYPE),
autoDelete: equal("selection", DELETE_STATUS_TYPE),
autoBump: equal("selection", BUMP_TYPE),
publishToCategory: equal("selection", PUBLISH_TO_CATEGORY_STATUS_TYPE),
autoDeleteReplies: equal("selection", DELETE_REPLIES_TYPE),
showTimeOnly: or("autoOpen", "autoDelete", "autoBump"),
showFutureDateInput: or(
"showTimeOnly",
"publishToCategory",
"autoClose",
"autoDeleteReplies"
statusType: readOnly("topicTimer.status_type"),
autoOpen: equal("statusType", OPEN_STATUS_TYPE),
autoClose: equal("statusType", CLOSE_STATUS_TYPE),
autoCloseAfterLastPost: equal(
"statusType",
CLOSE_AFTER_LAST_POST_STATUS_TYPE
),
autoDelete: equal("statusType", DELETE_STATUS_TYPE),
autoBump: equal("statusType", BUMP_TYPE),
publishToCategory: equal("statusType", PUBLISH_TO_CATEGORY_STATUS_TYPE),
autoDeleteReplies: equal("statusType", DELETE_REPLIES_TYPE),
showTimeOnly: or("autoOpen", "autoDelete", "autoBump"),
showFutureDateInput: or("showTimeOnly", "publishToCategory", "autoClose"),
useDuration: or(
"isBasedOnLastPost",
"autoDeleteReplies",
"autoCloseAfterLastPost"
),
duration: null,
@on("init")
preloadDuration() {
if (!this.useDuration || !this.topicTimer.duration_minutes) {
return;
}
if (this.durationType === "days") {
this.set("duration", this.topicTimer.duration_minutes / 60 / 24);
} else {
this.set("duration", this.topicTimer.duration_minutes / 60);
}
},
@discourseComputed("autoDeleteReplies")
durationType(autoDeleteReplies) {
@ -39,26 +61,134 @@ export default Component.extend({
}
},
@observes("selection")
_updateBasedOnLastPost() {
if (!this.autoClose) {
schedule("afterRender", () => {
this.set("topicTimer.based_on_last_post", false);
});
@discourseComputed()
customTimeShortcutOptions() {
return [
{
icon: "bed",
id: "this_weekend",
label: "topic.auto_update_input.this_weekend",
time: thisWeekend(),
timeFormatKey: "dates.time_short_day",
},
{
icon: "far-clock",
id: "two_weeks",
label: "topic.auto_update_input.two_weeks",
time: startOfDay(now().add(2, "weeks")),
timeFormatKey: "dates.long_no_year",
},
{
icon: "far-calendar-plus",
id: "three_months",
label: "topic.auto_update_input.three_months",
time: startOfDay(now().add(3, "months")),
timeFormatKey: "dates.long_no_year",
},
{
icon: "far-calendar-plus",
id: "six_months",
label: "topic.auto_update_input.six_months",
time: startOfDay(now().add(6, "months")),
timeFormatKey: "dates.long_no_year",
},
];
},
@discourseComputed
hiddenTimeShortcutOptions() {
return ["none", "start_of_next_business_week"];
},
isCustom: equal("timerType", "custom"),
isBasedOnLastPost: equal("statusType", "close_after_last_post"),
@discourseComputed(
"topicTimer.updateTime",
"topicTimer.duration_minutes",
"useDuration"
)
executeAt(updateTime, duration, useDuration) {
if (useDuration) {
return moment().add(parseFloat(duration), "minutes").format(FORMAT);
} else {
return updateTime;
}
},
didReceiveAttrs() {
this._super(...arguments);
@discourseComputed(
"isBasedOnLastPost",
"topicTimer.duration_minutes",
"topic.last_posted_at"
)
willCloseImmediately(isBasedOnLastPost, duration, lastPostedAt) {
if (isBasedOnLastPost && duration) {
let closeDate = moment(lastPostedAt);
closeDate = closeDate.add(duration, "minutes");
return closeDate < moment();
}
},
// TODO: get rid of this hack
schedule("afterRender", () => {
if (!this.get("topicTimer.status_type")) {
this.set(
"topicTimer.status_type",
this.get("timerTypes.firstObject.id")
);
@discourseComputed("isBasedOnLastPost", "topic.last_posted_at")
willCloseI18n(isBasedOnLastPost, lastPostedAt) {
if (isBasedOnLastPost) {
const diff = Math.round(
(new Date() - new Date(lastPostedAt)) / (1000 * 60 * 60)
);
return I18n.t("topic.auto_close_momentarily", { count: diff });
}
},
@discourseComputed("durationType")
durationLabel(durationType) {
return I18n.t(`topic.topic_status_update.num_of_${durationType}`);
},
@discourseComputed(
"statusType",
"isCustom",
"topicTimer.updateTime",
"willCloseImmediately",
"topicTimer.category_id",
"useDuration",
"topicTimer.duration_minutes"
)
showTopicTimerInfo(
statusType,
isCustom,
updateTime,
willCloseImmediately,
categoryId,
useDuration,
duration
) {
if (!statusType || willCloseImmediately) {
return false;
}
if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && isEmpty(categoryId)) {
return false;
}
if (isCustom && updateTime) {
if (moment(updateTime) < moment()) {
return false;
}
});
} else if (useDuration) {
return duration;
}
return updateTime;
},
@action
onTimeSelected(type, time) {
this.set("timerType", type);
this.onChangeInput(type, time);
},
@action
durationChanged(newDurationMins) {
this.set("topicTimer.duration_minutes", newDurationMins);
},
});

View File

@ -224,6 +224,7 @@ export default Component.extend({
if (event.target.value) {
results.innerHTML = emojiSearch(event.target.value.toLowerCase(), {
maxResults: 10,
diversity: this.emojiStore.diversity,
})
.map(this._replaceEmoji)
.join("");

View File

@ -1,42 +1,30 @@
import { and, empty, equal, or } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { and, empty, equal } from "@ember/object/computed";
import { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import I18n from "I18n";
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from "discourse/controllers/edit-topic-timer";
import { isEmpty } from "@ember/utils";
export default Component.extend({
selection: null,
date: null,
time: null,
includeDateTime: true,
duration: null,
durationType: "hours",
isCustom: equal("selection", "pick_date_and_time"),
isBasedOnLastPost: equal("selection", "set_based_on_last_post"),
displayDateAndTimePicker: and("includeDateTime", "isCustom"),
displayLabel: null,
labelClasses: null,
displayNumberInput: or("isBasedOnLastPost", "isBasedOnDuration"),
init() {
this._super(...arguments);
if (this.input) {
if (this.basedOnLastPost) {
this.set("selection", "set_based_on_last_post");
} else if (this.isBasedOnDuration) {
this.set("selection", null);
} else {
const datetime = moment(this.input);
this.setProperties({
selection: "pick_date_and_time",
date: datetime.format("YYYY-MM-DD"),
time: datetime.format("HH:mm"),
});
this._updateInput();
}
const datetime = moment(this.input);
this.setProperties({
selection: "pick_date_and_time",
date: datetime.format("YYYY-MM-DD"),
time: datetime.format("HH:mm"),
});
this._updateInput();
}
},
@ -59,49 +47,6 @@ export default Component.extend({
}
},
@observes("isBasedOnLastPost")
_updateBasedOnLastPost() {
this.set("basedOnLastPost", this.isBasedOnLastPost);
},
@observes("duration")
_updateDuration() {
this.attrs.onChangeDuration &&
this.attrs.onChangeDuration(parseInt(this.duration, 0));
},
@discourseComputed(
"input",
"duration",
"isBasedOnLastPost",
"isBasedOnDuration",
"durationType"
)
executeAt(
input,
duration,
isBasedOnLastPost,
isBasedOnDuration,
durationType
) {
if (isBasedOnLastPost || isBasedOnDuration) {
return moment(input)
.add(parseInt(duration, 0), durationType)
.format(FORMAT);
} else {
return input;
}
},
@discourseComputed("durationType")
durationLabel(durationType) {
return I18n.t(
`topic.topic_status_update.num_of_${
durationType === "hours" ? "hours" : "days"
}`
);
},
didReceiveAttrs() {
this._super(...arguments);
@ -109,65 +54,4 @@ export default Component.extend({
this.set("displayLabel", I18n.t(this.label));
}
},
@discourseComputed(
"statusType",
"input",
"isCustom",
"date",
"time",
"willCloseImmediately",
"categoryId",
"displayNumberInput",
"duration"
)
showTopicStatusInfo(
statusType,
input,
isCustom,
date,
time,
willCloseImmediately,
categoryId,
displayNumberInput,
duration
) {
if (!statusType || willCloseImmediately) {
return false;
}
if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && isEmpty(categoryId)) {
return false;
}
if (isCustom) {
if (date) {
return moment(`${date}${time ? " " + time : ""}`).isAfter(moment());
}
return time;
} else if (displayNumberInput) {
return duration;
} else {
return input;
}
},
@discourseComputed("isBasedOnLastPost", "input", "lastPostedAt")
willCloseImmediately(isBasedOnLastPost, input, lastPostedAt) {
if (isBasedOnLastPost && input) {
let closeDate = moment(lastPostedAt);
closeDate = closeDate.add(input, "hours");
return closeDate < moment();
}
},
@discourseComputed("isBasedOnLastPost", "lastPostedAt")
willCloseI18n(isBasedOnLastPost, lastPostedAt) {
if (isBasedOnLastPost) {
const diff = Math.round(
(new Date() - new Date(lastPostedAt)) / (1000 * 60 * 60)
);
return I18n.t("topic.auto_close_immediate", { count: diff });
}
},
});

View File

@ -11,7 +11,56 @@ export default DropdownSelectBoxComponent.extend({
showFullTitle: false,
},
content: computed("member.owner", function () {
contentBulk() {
const items = [];
items.push({
id: "removeMembers",
name: I18n.t("groups.members.remove_members"),
description: I18n.t("groups.members.remove_members_description"),
icon: "user-times",
});
if (this.bulkSelection.some((m) => !m.owner)) {
items.push({
id: "makeOwners",
name: I18n.t("groups.members.make_owners"),
description: I18n.t("groups.members.make_owners_description"),
icon: "shield-alt",
});
}
if (this.bulkSelection.some((m) => m.owner)) {
items.push({
id: "removeOwners",
name: I18n.t("groups.members.remove_owners"),
description: I18n.t("groups.members.remove_owners_description"),
icon: "shield-alt",
});
}
if (this.bulkSelection.some((m) => !m.primary)) {
items.push({
id: "setPrimary",
name: I18n.t("groups.members.make_all_primary"),
description: I18n.t("groups.members.make_all_primary_description"),
icon: "id-card",
});
}
if (this.bulkSelection.some((m) => m.primary)) {
items.push({
id: "unsetPrimary",
name: I18n.t("groups.members.remove_all_primary"),
description: I18n.t("groups.members.remove_all_primary_description"),
icon: "id-card",
});
}
return items;
},
contentSingle() {
const items = [
{
id: "removeMember",
@ -45,6 +94,39 @@ export default DropdownSelectBoxComponent.extend({
}
}
if (this.currentUser.staff) {
if (this.member.primary) {
items.push({
id: "removePrimary",
name: I18n.t("groups.members.remove_primary"),
description: I18n.t("groups.members.remove_primary_description", {
username: this.get("member.username"),
}),
icon: "id-card",
});
} else {
items.push({
id: "makePrimary",
name: I18n.t("groups.members.make_primary"),
description: I18n.t("groups.members.make_primary_description", {
username: this.get("member.username"),
}),
icon: "id-card",
});
}
}
return items;
}),
},
content: computed(
"bulkSelection.[]",
"member.owner",
"member.primary",
function () {
return this.bulkSelection !== undefined
? this.contentBulk()
: this.contentSingle();
}
),
});

View File

@ -20,9 +20,9 @@ export default Component.extend({
isStaff: readOnly("currentUser.staff"),
isAdmin: readOnly("currentUser.admin"),
// If this isn't defined, it will proxy to the user topic on the preferences
// page which is wrong.
emailOrUsername: null,
// invitee is either a user, group or email
invitee: null,
isInviteeGroup: false,
hasCustomMessage: false,
customMessage: null,
inviteIcon: "envelope",
@ -41,7 +41,7 @@ export default Component.extend({
@discourseComputed(
"isAdmin",
"emailOrUsername",
"invitee",
"invitingToTopic",
"isPrivateTopic",
"groupIds",
@ -50,7 +50,7 @@ export default Component.extend({
)
disabled(
isAdmin,
emailOrUsername,
invitee,
invitingToTopic,
isPrivateTopic,
groupIds,
@ -60,24 +60,22 @@ export default Component.extend({
if (saving) {
return true;
}
if (isEmpty(emailOrUsername)) {
if (isEmpty(invitee)) {
return true;
}
const emailTrimmed = emailOrUsername.trim();
// when inviting to forum, email must be valid
if (!invitingToTopic && !emailValid(emailTrimmed)) {
if (!invitingToTopic && !emailValid(invitee)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) {
if (!isAdmin && isPrivateTopic && emailValid(invitee)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && isEmpty(groupIds) && emailValid(emailTrimmed)) {
if (isPrivateTopic && isEmpty(groupIds) && emailValid(invitee)) {
return true;
}
@ -90,7 +88,7 @@ export default Component.extend({
@discourseComputed(
"isAdmin",
"emailOrUsername",
"invitee",
"inviteModel.saving",
"isPrivateTopic",
"groupIds",
@ -98,7 +96,7 @@ export default Component.extend({
)
disabledCopyLink(
isAdmin,
emailOrUsername,
invitee,
saving,
isPrivateTopic,
groupIds,
@ -110,24 +108,22 @@ export default Component.extend({
if (saving) {
return true;
}
if (isEmpty(emailOrUsername)) {
if (isEmpty(invitee)) {
return true;
}
const email = emailOrUsername.trim();
// email must be valid
if (!emailValid(email)) {
if (!emailValid(invitee)) {
return true;
}
// normal users (not admin) can't invite users to private topic via email
if (!isAdmin && isPrivateTopic && emailValid(email)) {
if (!isAdmin && isPrivateTopic && emailValid(invitee)) {
return true;
}
// when inviting to private topic via email, group name must be specified
if (isPrivateTopic && isEmpty(groupIds) && emailValid(email)) {
if (isPrivateTopic && isEmpty(groupIds) && emailValid(invitee)) {
return true;
}
@ -179,7 +175,7 @@ export default Component.extend({
// Show Groups? (add invited user to private group)
@discourseComputed(
"isGroupOwnerOrAdmin",
"emailOrUsername",
"invitee",
"isPrivateTopic",
"isPM",
"invitingToTopic",
@ -187,7 +183,7 @@ export default Component.extend({
)
showGroups(
isGroupOwnerOrAdmin,
emailOrUsername,
invitee,
isPrivateTopic,
isPM,
invitingToTopic,
@ -197,20 +193,20 @@ export default Component.extend({
isGroupOwnerOrAdmin &&
canInviteViaEmail &&
!isPM &&
(emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic)
(emailValid(invitee) || isPrivateTopic || !invitingToTopic)
);
},
@discourseComputed("emailOrUsername")
showCustomMessage(emailOrUsername) {
return this.inviteModel === this.currentUser || emailValid(emailOrUsername);
@discourseComputed("invitee")
showCustomMessage(invitee) {
return this.inviteModel === this.currentUser || emailValid(invitee);
},
// Instructional text for the modal.
@discourseComputed(
"isPM",
"invitingToTopic",
"emailOrUsername",
"invitee",
"isPrivateTopic",
"isAdmin",
"canInviteViaEmail"
@ -218,14 +214,14 @@ export default Component.extend({
inviteInstructions(
isPM,
invitingToTopic,
emailOrUsername,
invitee,
isPrivateTopic,
isAdmin,
canInviteViaEmail
) {
if (!canInviteViaEmail) {
// can't invite via email, only existing users
return I18n.t("topic.invite_reply.sso_enabled");
return I18n.t("topic.invite_reply.discourse_connect_enabled");
} else if (isPM) {
// inviting to a message
return I18n.t("topic.invite_private.email_or_username");
@ -236,9 +232,9 @@ export default Component.extend({
return I18n.t("topic.invite_reply.to_username");
} else {
// when inviting to a topic, display instructions based on provided entity
if (isEmpty(emailOrUsername)) {
if (isEmpty(invitee)) {
return I18n.t("topic.invite_reply.to_topic_blank");
} else if (emailValid(emailOrUsername)) {
} else if (emailValid(invitee)) {
this.set("inviteIcon", "envelope");
return I18n.t("topic.invite_reply.to_topic_email");
} else {
@ -257,18 +253,18 @@ export default Component.extend({
return isPrivateTopic ? "required" : "optional";
},
@discourseComputed("isPM", "emailOrUsername", "invitingExistingUserToTopic")
successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) {
if (this.hasGroups) {
@discourseComputed("isPM", "invitee", "invitingExistingUserToTopic")
successMessage(isPM, invitee, invitingExistingUserToTopic) {
if (this.isInviteeGroup) {
return I18n.t("topic.invite_private.success_group");
} else if (isPM) {
return I18n.t("topic.invite_private.success");
} else if (invitingExistingUserToTopic) {
return I18n.t("topic.invite_reply.success_existing_email", {
emailOrUsername,
invitee,
});
} else if (emailValid(emailOrUsername)) {
return I18n.t("topic.invite_reply.success_email", { emailOrUsername });
} else if (emailValid(invitee)) {
return I18n.t("topic.invite_reply.success_email", { invitee });
} else {
return I18n.t("topic.invite_reply.success_username");
}
@ -295,7 +291,8 @@ export default Component.extend({
// Reset the modal to allow a new user to be invited.
reset() {
this.setProperties({
emailOrUsername: null,
invitee: null,
isInviteeGroup: false,
hasCustomMessage: false,
customMessage: null,
invitingExistingUserToTopic: false,
@ -346,9 +343,9 @@ export default Component.extend({
model.setProperties({ saving: false, error: true });
};
if (this.hasGroups) {
if (this.isInviteeGroup) {
return this.inviteModel
.createGroupInvite(this.emailOrUsername.trim())
.createGroupInvite(this.invitee.trim())
.then((data) => {
model.setProperties({ saving: false, finished: true });
this.get("inviteModel.details.allowed_groups").pushObject(
@ -359,7 +356,7 @@ export default Component.extend({
.catch(onerror);
} else {
return this.inviteModel
.createInvite(this.emailOrUsername.trim(), groupIds, this.customMessage)
.createInvite(this.invitee.trim(), groupIds, this.customMessage)
.then((result) => {
model.setProperties({ saving: false, finished: true });
if (!this.invitingToTopic && userInvitedController) {
@ -379,7 +376,7 @@ export default Component.extend({
this.appEvents.trigger("post-stream:refresh", { force: true });
} else if (
this.invitingToTopic &&
emailValid(this.emailOrUsername.trim()) &&
emailValid(this.invitee.trim()) &&
result &&
result.user
) {
@ -407,7 +404,7 @@ export default Component.extend({
}
return model
.generateInviteLink(this.emailOrUsername.trim(), groupIds, topicId)
.generateInviteLink(this.invitee.trim(), groupIds, topicId)
.then((result) => {
model.setProperties({
saving: false,
@ -465,7 +462,23 @@ export default Component.extend({
@action
searchContact() {
getNativeContact(this.capabilities, ["email"], false).then((result) => {
this.set("emailOrUsername", result[0].email[0]);
this.set("invitee", result[0].email[0]);
});
},
@action
updateInvitee(selected, content) {
const invitee = content.findBy("id", selected[0]);
if (invitee) {
this.setProperties({
invitee: invitee.id.trim(),
isInviteeGroup: invitee.isGroup || false,
});
} else {
this.setProperties({
invitee: null,
isInviteeGroup: false,
});
}
},
});

View File

@ -0,0 +1,154 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import { action } from "@ember/object";
export default Component.extend({
tagName: "",
selectedInterval: "mins",
durationMinutes: null,
durationHours: null,
duration: null,
hiddenIntervals: null,
@on("init")
cloneDuration() {
let mins = this.durationMinutes;
let hours = this.durationHours;
if (hours && mins) {
throw new Error(
"relative-time needs initial duration in hours OR minutes, both are not supported"
);
}
if (hours) {
this._setInitialDurationFromHours(hours);
} else {
this._setInitialDurationFromMinutes(mins);
}
},
@on("init")
setHiddenIntervals() {
this.hiddenIntervals = this.hiddenIntervals || [];
},
_setInitialDurationFromHours(hours) {
if (hours >= 730) {
this.setProperties({
duration: Math.floor(hours / 30 / 24),
selectedInterval: "months",
});
} else if (hours >= 24) {
this.setProperties({
duration: Math.floor(hours / 24),
selectedInterval: "days",
});
} else {
this.setProperties({
duration: hours,
selectedInterval: "hours",
});
}
},
_setInitialDurationFromMinutes(mins) {
if (mins >= 43800) {
this.setProperties({
duration: Math.floor(mins / 30 / 60 / 24),
selectedInterval: "months",
});
} else if (mins >= 1440) {
this.setProperties({
duration: Math.floor(mins / 60 / 24),
selectedInterval: "days",
});
} else if (mins >= 60) {
this.setProperties({
duration: Math.floor(mins / 60),
selectedInterval: "hours",
});
} else {
this.setProperties({
duration: mins,
selectedInterval: "mins",
});
}
},
@discourseComputed("selectedInterval")
durationMin(selectedInterval) {
return selectedInterval === "mins" ? 1 : 0.1;
},
@discourseComputed("selectedInterval")
durationStep(selectedInterval) {
return selectedInterval === "mins" ? 1 : 0.05;
},
@discourseComputed("duration")
intervals(duration) {
const count = duration ? parseFloat(duration) : 0;
return [
{
id: "mins",
name: I18n.t("relative_time_picker.minutes", { count }),
},
{
id: "hours",
name: I18n.t("relative_time_picker.hours", { count }),
},
{
id: "days",
name: I18n.t("relative_time_picker.days", { count }),
},
{
id: "months",
name: I18n.t("relative_time_picker.months", { count }),
},
].filter((interval) => !this.hiddenIntervals.includes(interval.id));
},
@discourseComputed("selectedInterval", "duration")
calculatedMinutes(interval, duration) {
duration = parseFloat(duration);
let mins = 0;
switch (interval) {
case "mins":
// we round up here in case the user manually inputted a step < 1
mins = Math.ceil(duration);
break;
case "hours":
mins = duration * 60;
break;
case "days":
mins = duration * 60 * 24;
break;
case "months":
mins = duration * 60 * 24 * 30; // least accurate because of varying days in months
break;
}
return mins;
},
@action
onChangeInterval(newInterval) {
this.set("selectedInterval", newInterval);
if (this.onChange) {
this.onChange(this.calculatedMinutes);
}
},
@action
onChangeDuration() {
if (this.onChange) {
this.onChange(this.calculatedMinutes);
}
},
});

View File

@ -0,0 +1,277 @@
import {
START_OF_DAY_HOUR,
laterToday,
now,
parseCustomDatetime,
} from "discourse/lib/time-utils";
import {
TIME_SHORTCUT_TYPES,
defaultShortcutOptions,
} from "discourse/lib/time-shortcut";
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import { action } from "@ember/object";
import { and, equal } from "@ember/object/computed";
// global shortcuts that interfere with these modal shortcuts, they are rebound when the
// component is destroyed
//
// c createTopic
// r replyToPost
// l toggle like
// t replyAsNewTopic
const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "t"];
const BINDINGS = {
"l t": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.LATER_TODAY],
},
"l w": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.LATER_THIS_WEEK],
},
"n d": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.TOMORROW],
},
"n w": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.NEXT_WEEK],
},
"n b w": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK],
},
"n m": {
handler: "selectShortcut",
args: [TIME_SHORTCUT_TYPES.NEXT_MONTH],
},
"c r": { handler: "selectShortcut", args: [TIME_SHORTCUT_TYPES.CUSTOM] },
"n r": { handler: "selectShortcut", args: [TIME_SHORTCUT_TYPES.NONE] },
};
export default Component.extend({
tagName: "",
userTimezone: null,
onTimeSelected: null,
selectedShortcut: null,
selectedTime: null,
selectedDate: null,
selectedDatetime: null,
prefilledDatetime: null,
additionalOptionsToShow: null,
hiddenOptions: null,
customOptions: null,
lastCustomDate: null,
lastCustomTime: null,
parsedLastCustomDatetime: null,
customDate: null,
customTime: null,
defaultCustomReminderTime: `0${START_OF_DAY_HOUR}:00`,
@on("init")
_setupPicker() {
this.setProperties({
customTime: this.defaultCustomReminderTime,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
additionalOptionsToShow: this.additionalOptionsToShow || [],
hiddenOptions: this.hiddenOptions || [],
customOptions: this.customOptions || [],
});
if (this.prefilledDatetime) {
this.parsePrefilledDatetime();
}
this._bindKeyboardShortcuts();
this._loadLastUsedCustomDatetime();
},
@observes("prefilledDatetime")
prefilledDatetimeChanged() {
if (this.prefilledDatetime) {
this.parsePrefilledDatetime();
} else {
this.setProperties({
customDate: null,
customTime: null,
selectedShortcut: null,
});
}
},
@on("willDestroyElement")
_resetKeyboardShortcuts() {
KeyboardShortcuts.unbind(BINDINGS);
KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE);
},
parsePrefilledDatetime() {
let parsedDatetime = parseCustomDatetime(
this.prefilledDatetime,
null,
this.userTimezone
);
if (parsedDatetime.isSame(laterToday())) {
return this.set("selectedShortcut", TIME_SHORTCUT_TYPES.LATER_TODAY);
}
this.setProperties({
customDate: parsedDatetime.format("YYYY-MM-DD"),
customTime: parsedDatetime.format("HH:mm"),
selectedShortcut: TIME_SHORTCUT_TYPES.CUSTOM,
});
},
_loadLastUsedCustomDatetime() {
let lastTime = localStorage.lastCustomTime;
let lastDate = localStorage.lastCustomDate;
if (lastTime && lastDate) {
let parsed = parseCustomDatetime(lastDate, lastTime, this.userTimezone);
if (parsed < now(this.userTimezone)) {
return;
}
this.setProperties({
lastCustomDate: lastDate,
lastCustomTime: lastTime,
parsedLastCustomDatetime: parsed,
});
}
},
_bindKeyboardShortcuts() {
KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE);
Object.keys(BINDINGS).forEach((shortcut) => {
KeyboardShortcuts.addShortcut(shortcut, () => {
let binding = BINDINGS[shortcut];
if (binding.args) {
return this.send(binding.handler, ...binding.args);
}
this.send(binding.handler);
});
});
},
customDatetimeSelected: equal("selectedShortcut", TIME_SHORTCUT_TYPES.CUSTOM),
relativeTimeSelected: equal("selectedShortcut", TIME_SHORTCUT_TYPES.RELATIVE),
customDatetimeFilled: and("customDate", "customTime"),
@observes("customDate", "customTime")
customDatetimeChanged() {
if (!this.customDatetimeFilled) {
return;
}
this.selectShortcut(TIME_SHORTCUT_TYPES.CUSTOM);
},
@discourseComputed(
"additionalOptionsToShow",
"hiddenOptions",
"customOptions",
"userTimezone"
)
options(additionalOptionsToShow, hiddenOptions, customOptions, userTimezone) {
let options = defaultShortcutOptions(userTimezone);
if (additionalOptionsToShow.length > 0) {
options.forEach((opt) => {
if (additionalOptionsToShow.includes(opt.id)) {
opt.hidden = false;
}
});
}
if (hiddenOptions.length > 0) {
options.forEach((opt) => {
if (hiddenOptions.includes(opt.id)) {
opt.hidden = true;
}
});
}
if (this.lastCustomDate && this.lastCustomTime) {
let lastCustom = options.findBy("id", TIME_SHORTCUT_TYPES.LAST_CUSTOM);
lastCustom.time = this.parsedLastCustomDatetime;
lastCustom.timeFormatted = this.parsedLastCustomDatetime.format(
I18n.t("dates.long_no_year")
);
lastCustom.hidden = false;
}
customOptions.forEach((opt) => {
if (!opt.timeFormatted && opt.time) {
opt.timeFormatted = opt.time.format(I18n.t(opt.timeFormatKey));
}
});
let relativeOptionIndex = options.findIndex(
(opt) => opt.id === TIME_SHORTCUT_TYPES.RELATIVE
);
options.splice(relativeOptionIndex, 0, ...customOptions);
return options;
},
@action
relativeTimeChanged(relativeTimeMins) {
let dateTime = now(this.userTimezone).add(relativeTimeMins, "minutes");
this.set("selectedDatetime", dateTime);
if (this.onTimeSelected) {
this.onTimeSelected(TIME_SHORTCUT_TYPES.RELATIVE, dateTime);
}
},
@action
selectShortcut(type) {
if (this.options.filterBy("hidden").mapBy("id").includes(type)) {
return;
}
let dateTime = null;
if (type === TIME_SHORTCUT_TYPES.CUSTOM) {
this.set("customTime", this.customTime || this.defaultCustomReminderTime);
const customDatetime = parseCustomDatetime(
this.customDate,
this.customTime,
this.userTimezone
);
if (customDatetime.isValid()) {
dateTime = customDatetime;
localStorage.lastCustomTime = this.customTime;
localStorage.lastCustomDate = this.customDate;
}
} else {
dateTime = this.options.findBy("id", type).time;
}
this.setProperties({
selectedShortcut: type,
selectedDatetime: dateTime,
});
if (this.onTimeSelected) {
this.onTimeSelected(type, dateTime);
}
},
});

View File

@ -2,6 +2,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import { RUNTIME_OPTIONS } from "discourse-common/lib/raw-handlebars-helpers";
import { alias } from "@ember/object/computed";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { on } from "@ember/object/evented";
@ -49,7 +50,10 @@ export default Component.extend({
renderTopicListItem() {
const template = findRawTemplate("list/topic-list-item");
if (template) {
this.set("topicListItemContents", template(this).htmlSafe());
this.set(
"topicListItemContents",
template(this, RUNTIME_OPTIONS).htmlSafe()
);
}
},

View File

@ -185,6 +185,10 @@ export default Component.extend({
"margin-bottom",
!isDocked && composerHeight > draftComposerHeight ? "0px" : ""
);
this.appEvents.trigger("topic-progress:docked-status-changed", {
docked: isDocked,
element: this.element,
});
},
click(e) {

View File

@ -3,28 +3,46 @@ import Category from "discourse/models/category";
import Component from "@ember/component";
import { DELETE_REPLIES_TYPE } from "discourse/controllers/edit-topic-timer";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import discourseComputed, { on } from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { isTesting } from "discourse-common/config/environment";
export default Component.extend({
classNames: ["topic-status-info"],
classNames: ["topic-timer-info"],
_delayedRerender: null,
clockIcon: `${iconHTML("far-clock")}`.htmlSafe(),
trashCanIcon: `${iconHTML("trash-alt")}`.htmlSafe(),
trashCanTitle: I18n.t("post.controls.remove_timer"),
trashLabel: I18n.t("post.controls.remove_timer"),
title: null,
notice: null,
showTopicTimer: null,
showTopicTimerModal: null,
removeTopicTimer: null,
@on("didReceiveAttrs")
setupRenderer() {
this.renderTopicTimer();
},
@on("willDestroyElement")
cancelDelayedRenderer() {
if (this._delayedRerender) {
cancel(this._delayedRerender);
}
},
@discourseComputed
canRemoveTimer() {
canModifyTimer() {
return this.currentUser && this.currentUser.get("canManageTopic");
},
@discourseComputed("canRemoveTimer", "removeTopicTimer")
showTrashCan(canRemoveTimer, removeTopicTimer) {
return canRemoveTimer && removeTopicTimer;
@discourseComputed("canModifyTimer", "removeTopicTimer")
showTrashCan(canModifyTimer, removeTopicTimer) {
return canModifyTimer && removeTopicTimer;
},
@discourseComputed("canModifyTimer", "showTopicTimerModal")
showEdit(canModifyTimer, showTopicTimerModal) {
return canModifyTimer && showTopicTimerModal;
},
renderTopicTimer() {
@ -39,6 +57,10 @@ export default Component.extend({
return;
}
if (this.isDestroyed) {
return;
}
const topicStatus = this.topicClosed ? "close" : "open";
const topicStatusKnown = this.topicClosed !== undefined;
if (topicStatusKnown && topicStatus === this.statusType) {
@ -59,15 +81,13 @@ export default Component.extend({
} else if (minutesLeft > 2) {
rerenderDelay = 60000;
}
let durationHours = parseInt(this.duration, 0) || 0;
if (isDeleteRepliesType) {
durationHours *= 24;
}
let durationMinutes = parseInt(this.durationMinutes, 0) || 0;
let options = {
timeLeft: duration.humanize(true),
duration: moment.duration(durationHours, "hours").humanize(),
duration: moment
.duration(durationMinutes, "minutes")
.humanize({ s: 60, m: 60, h: 24 }),
};
const categoryId = this.categoryId;
@ -100,41 +120,15 @@ export default Component.extend({
}
},
didReceiveAttrs() {
this._super(...arguments);
this.renderTopicTimer();
},
didInsertElement() {
this._super(...arguments);
if (this.removeTopicTimer) {
$(this.element).on(
"click.topic-timer-remove",
"button",
this.removeTopicTimer
);
}
},
willDestroyElement() {
$(this.element).off("click.topic-timer-remove", this.removeTopicTimer);
if (this._delayedRerender) {
cancel(this._delayedRerender);
}
},
_noticeKey() {
let statusType = this.statusType;
if (statusType === "silent_close") {
statusType = "close";
}
if (this.basedOnLastPost) {
return `topic.status_update_notice.auto_${statusType}_based_on_last_post`;
} else {
return `topic.status_update_notice.auto_${statusType}`;
if (this.basedOnLastPost && statusType === "close") {
statusType = "close_after_last_post";
}
return `topic.status_update_notice.auto_${statusType}`;
},
});

View File

@ -2,7 +2,7 @@ import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
tagName: "span",
tagName: "",
@discourseComputed("count")
showGrantCount(count) {

View File

@ -3,6 +3,7 @@ import TextField from "discourse/components/text-field";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { isEmpty } from "@ember/utils";
import userSearch from "discourse/lib/user-search";
import deprecated from "discourse-common/lib/deprecated";
export default TextField.extend({
autocorrect: false,
@ -12,6 +13,14 @@ export default TextField.extend({
single: false,
fullWidthWrap: false,
@on("init")
deprecateComponent() {
deprecated(
"`{{user-selector}}` is deprecated. Please use `{{email-group-user-chooser}}` instead.",
{ since: "2.7", dropFrom: "2.8" }
);
},
@bind
_paste(event) {
let pastedText = "";

View File

@ -13,7 +13,7 @@ export default Controller.extend({
return (
!this.siteSettings.invite_only &&
this.siteSettings.allow_new_registrations &&
!this.siteSettings.enable_sso
!this.siteSettings.enable_discourse_connect
);
},

View File

@ -1,105 +1,18 @@
import { REMINDER_TYPES, formattedReminderTime } from "discourse/lib/bookmark";
import { isEmpty, isPresent } from "@ember/utils";
import { next, schedule } from "@ember/runloop";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import Controller from "@ember/controller";
import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { Promise } from "rsvp";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { and } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
// global shortcuts that interfere with these modal shortcuts, they are rebound when the
// modal is closed
//
// c createTopic
// r replyToPost
// l toggle like
// d deletePost
// t replyAsNewTopic
const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "d", "t"];
const START_OF_DAY_HOUR = 8;
const LATER_TODAY_CUTOFF_HOUR = 17;
const LATER_TODAY_MAX_HOUR = 18;
const MOMENT_MONDAY = 1;
const MOMENT_THURSDAY = 4;
const BOOKMARK_BINDINGS = {
enter: { handler: "saveAndClose" },
"l t": { handler: "selectReminderType", args: [REMINDER_TYPES.LATER_TODAY] },
"l w": {
handler: "selectReminderType",
args: [REMINDER_TYPES.LATER_THIS_WEEK],
},
"n b d": {
handler: "selectReminderType",
args: [REMINDER_TYPES.NEXT_BUSINESS_DAY],
},
"n d": { handler: "selectReminderType", args: [REMINDER_TYPES.TOMORROW] },
"n w": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_WEEK] },
"n b w": {
handler: "selectReminderType",
args: [REMINDER_TYPES.START_OF_NEXT_BUSINESS_WEEK],
},
"n m": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_MONTH] },
"c r": { handler: "selectReminderType", args: [REMINDER_TYPES.CUSTOM] },
"n r": { handler: "selectReminderType", args: [REMINDER_TYPES.NONE] },
"d d": { handler: "delete" },
};
export default Controller.extend(ModalFunctionality, {
loading: false,
errorMessage: null,
selectedReminderType: null,
_closeWithoutSaving: false,
_savingBookmarkManually: false,
onCloseWithoutSaving: null,
customReminderDate: null,
customReminderTime: null,
lastCustomReminderDate: null,
lastCustomReminderTime: null,
postLocalDate: null,
mouseTrap: null,
userTimezone: null,
showOptions: false,
onShow() {
this.setProperties({
errorMessage: null,
selectedReminderType: REMINDER_TYPES.NONE,
_closeWithoutSaving: false,
_savingBookmarkManually: false,
customReminderDate: null,
customReminderTime: this._defaultCustomReminderTime(),
lastCustomReminderDate: null,
lastCustomReminderTime: null,
postDetectedLocalDate: null,
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
showOptions: false,
model: this.model || {},
allowSave: true,
});
},
this._loadBookmarkOptions();
this._bindKeyboardShortcuts();
this._loadLastUsedCustomReminderDatetime();
if (this._editingExistingBookmark()) {
this._initializeExistingBookmarkData();
}
this.loadLocalDates();
schedule("afterRender", () => {
if (this.site.isMobileDevice) {
document.getElementById("bookmark-name").blur();
}
});
@action
registerOnCloseHandler(handlerFn) {
this.set("onCloseHandler", handlerFn);
},
/**
@ -107,473 +20,8 @@ export default Controller.extend(ModalFunctionality, {
* clicks the save or cancel button to mimic browser behaviour.
*/
onClose(opts = {}) {
if (opts.initiatedByCloseButton) {
this._closeWithoutSaving = true;
if (this.onCloseHandler) {
this.onCloseHandler(opts.initiatedByCloseButton);
}
this._unbindKeyboardShortcuts();
this._restoreGlobalShortcuts();
if (!this._closeWithoutSaving && !this._savingBookmarkManually) {
this._saveBookmark().catch((e) => this._handleSaveError(e));
}
if (this.onCloseWithoutSaving && this._closeWithoutSaving) {
this.onCloseWithoutSaving();
}
},
_initializeExistingBookmarkData() {
if (this._existingBookmarkHasReminder()) {
let parsedReminderAt = this._parseCustomDateTime(this.model.reminderAt);
if (parsedReminderAt.isSame(this.laterToday())) {
return this.set("selectedReminderType", REMINDER_TYPES.LATER_TODAY);
}
this.setProperties({
customReminderDate: parsedReminderAt.format("YYYY-MM-DD"),
customReminderTime: parsedReminderAt.format("HH:mm"),
selectedReminderType: REMINDER_TYPES.CUSTOM,
});
}
},
_editingExistingBookmark() {
return isPresent(this.model) && isPresent(this.model.id);
},
_existingBookmarkHasReminder() {
return isPresent(this.model) && isPresent(this.model.reminderAt);
},
_loadBookmarkOptions() {
this.set(
"autoDeletePreference",
this.model.autoDeletePreference || this._preferredDeleteOption() || 0
);
// we want to make sure the options panel opens so the user
// knows they have set these options previously. run next otherwise
// the modal is not visible when it tries to slide down the options
if (this.autoDeletePreference) {
next(() => this.toggleOptionsPanel());
}
},
_preferredDeleteOption() {
let preferred = localStorage.bookmarkDeleteOption;
if (preferred && preferred !== "") {
preferred = parseInt(preferred, 10);
}
return preferred;
},
_loadLastUsedCustomReminderDatetime() {
let lastTime = localStorage.lastCustomBookmarkReminderTime;
let lastDate = localStorage.lastCustomBookmarkReminderDate;
if (lastTime && lastDate) {
let parsed = this._parseCustomDateTime(lastDate, lastTime);
// can't set reminders in the past
if (parsed < this.now()) {
return;
}
this.setProperties({
lastCustomReminderDate: lastDate,
lastCustomReminderTime: lastTime,
parsedLastCustomReminderDatetime: parsed,
});
}
},
_bindKeyboardShortcuts() {
KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE);
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
KeyboardShortcuts.addShortcut(shortcut, () => {
let binding = BOOKMARK_BINDINGS[shortcut];
if (binding.args) {
return this.send(binding.handler, ...binding.args);
}
this.send(binding.handler);
});
});
},
_unbindKeyboardShortcuts() {
KeyboardShortcuts.unbind(BOOKMARK_BINDINGS);
},
_restoreGlobalShortcuts() {
KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE);
},
@discourseComputed("model.reminderAt")
showExistingReminderAt(existingReminderAt) {
return isPresent(existingReminderAt);
},
@discourseComputed("model.id")
showDelete(id) {
return isPresent(id);
},
@discourseComputed("selectedReminderType")
customDateTimeSelected(selectedReminderType) {
return selectedReminderType === REMINDER_TYPES.CUSTOM;
},
@discourseComputed()
reminderTypes: () => {
return REMINDER_TYPES;
},
@discourseComputed()
autoDeletePreferences: () => {
return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => {
return {
id: AUTO_DELETE_PREFERENCES[key],
name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`),
};
});
},
showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"),
get showLaterToday() {
let later = this.laterToday();
return (
!later.isSame(this.tomorrow(), "date") &&
this.now().hour() < LATER_TODAY_CUTOFF_HOUR
);
},
get showLaterThisWeek() {
return this.now().day() < MOMENT_THURSDAY;
},
@discourseComputed("parsedLastCustomReminderDatetime")
lastCustomFormatted(parsedLastCustomReminderDatetime) {
return parsedLastCustomReminderDatetime.format(
I18n.t("dates.long_no_year")
);
},
@discourseComputed("model.reminderAt")
existingReminderAtFormatted(existingReminderAt) {
return formattedReminderTime(existingReminderAt, this.userTimezone);
},
get startNextBusinessWeekLabel() {
if (this.now().day() === MOMENT_MONDAY) {
return I18n.t("bookmarks.reminders.start_of_next_business_week_alt");
}
return I18n.t("bookmarks.reminders.start_of_next_business_week");
},
get startNextBusinessWeekFormatted() {
return this.nextWeek()
.day(MOMENT_MONDAY)
.format(I18n.t("dates.long_no_year"));
},
get laterTodayFormatted() {
return this.laterToday().format(I18n.t("dates.time"));
},
get tomorrowFormatted() {
return this.tomorrow().format(I18n.t("dates.time_short_day"));
},
get nextWeekFormatted() {
return this.nextWeek().format(I18n.t("dates.long_no_year"));
},
get laterThisWeekFormatted() {
return this.laterThisWeek().format(I18n.t("dates.time_short_day"));
},
get nextMonthFormatted() {
return this.nextMonth().format(I18n.t("dates.long_no_year"));
},
loadLocalDates() {
let postEl = document.querySelector(
`[data-post-id="${this.model.postId}"]`
);
let localDateEl = null;
if (postEl) {
localDateEl = postEl.querySelector(".discourse-local-date");
}
if (localDateEl) {
this.setProperties({
postDetectedLocalDate: localDateEl.dataset.date,
postDetectedLocalTime: localDateEl.dataset.time,
postDetectedLocalTimezone: localDateEl.dataset.timezone,
});
}
},
@discourseComputed("userTimezone")
userHasTimezoneSet(userTimezone) {
return !isEmpty(userTimezone);
},
_saveBookmark() {
const reminderAt = this._reminderAt();
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
if (this.selectedReminderType === REMINDER_TYPES.CUSTOM) {
if (!reminderAt) {
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
}
localStorage.lastCustomBookmarkReminderTime = this.customReminderTime;
localStorage.lastCustomBookmarkReminderDate = this.customReminderDate;
}
localStorage.bookmarkDeleteOption = this.autoDeletePreference;
let reminderType;
if (this.selectedReminderType === REMINDER_TYPES.NONE) {
reminderType = null;
} else if (
this.selectedReminderType === REMINDER_TYPES.LAST_CUSTOM ||
this.selectedReminderType === REMINDER_TYPES.POST_LOCAL_DATE
) {
reminderType = REMINDER_TYPES.CUSTOM;
} else {
reminderType = this.selectedReminderType;
}
const data = {
reminder_type: reminderType,
reminder_at: reminderAtISO,
name: this.model.name,
post_id: this.model.postId,
id: this.model.id,
auto_delete_preference: this.autoDeletePreference,
};
if (this._editingExistingBookmark()) {
return ajax("/bookmarks/" + this.model.id, {
type: "PUT",
data,
}).then(() => {
if (this.afterSave) {
this.afterSave({
reminderAt: reminderAtISO,
reminderType: this.selectedReminderType,
autoDeletePreference: this.autoDeletePreference,
id: this.model.id,
name: this.model.name,
});
}
});
} else {
return ajax("/bookmarks", { type: "POST", data }).then((response) => {
if (this.afterSave) {
this.afterSave({
reminderAt: reminderAtISO,
reminderType: this.selectedReminderType,
autoDeletePreference: this.autoDeletePreference,
id: response.id,
name: this.model.name,
});
}
});
}
},
_deleteBookmark() {
return ajax("/bookmarks/" + this.model.id, {
type: "DELETE",
}).then((response) => {
if (this.afterDelete) {
this.afterDelete(response.topic_bookmarked);
}
});
},
_parseCustomDateTime(date, time, parseTimezone = this.userTimezone) {
let dateTime = isPresent(time) ? date + " " + time : date;
let parsed = moment.tz(dateTime, parseTimezone);
if (parseTimezone !== this.userTimezone) {
parsed = parsed.tz(this.userTimezone);
}
return parsed;
},
_defaultCustomReminderTime() {
return `0${START_OF_DAY_HOUR}:00`;
},
_reminderAt() {
if (!this.selectedReminderType) {
return;
}
switch (this.selectedReminderType) {
case REMINDER_TYPES.LATER_TODAY:
return this.laterToday();
case REMINDER_TYPES.NEXT_BUSINESS_DAY:
return this.nextBusinessDay();
case REMINDER_TYPES.TOMORROW:
return this.tomorrow();
case REMINDER_TYPES.NEXT_WEEK:
return this.nextWeek();
case REMINDER_TYPES.START_OF_NEXT_BUSINESS_WEEK:
return this.nextWeek().day(MOMENT_MONDAY);
case REMINDER_TYPES.LATER_THIS_WEEK:
return this.laterThisWeek();
case REMINDER_TYPES.NEXT_MONTH:
return this.nextMonth();
case REMINDER_TYPES.CUSTOM:
this.set(
"customReminderTime",
this.customReminderTime || this._defaultCustomReminderTime()
);
const customDateTime = this._parseCustomDateTime(
this.customReminderDate,
this.customReminderTime
);
if (!customDateTime.isValid()) {
this.setProperties({
customReminderTime: null,
customReminderDate: null,
});
return;
}
return customDateTime;
case REMINDER_TYPES.LAST_CUSTOM:
return this.parsedLastCustomReminderDatetime;
case REMINDER_TYPES.POST_LOCAL_DATE:
return this.postLocalDate;
}
},
nextWeek() {
return this.startOfDay(this.now().add(7, "days"));
},
nextMonth() {
return this.startOfDay(this.now().add(1, "month"));
},
tomorrow() {
return this.startOfDay(this.now().add(1, "day"));
},
startOfDay(momentDate) {
return momentDate.hour(START_OF_DAY_HOUR).startOf("hour");
},
now() {
return moment.tz(this.userTimezone);
},
laterToday() {
let later = this.now().add(3, "hours");
if (later.hour() >= LATER_TODAY_MAX_HOUR) {
return later.hour(LATER_TODAY_MAX_HOUR).startOf("hour");
}
return later.minutes() < 30
? later.startOf("hour")
: later.add(30, "minutes").startOf("hour");
},
laterThisWeek() {
if (!this.showLaterThisWeek) {
return;
}
return this.startOfDay(this.now().add(2, "days"));
},
_handleSaveError(e) {
this._savingBookmarkManually = false;
if (typeof e === "string") {
bootbox.alert(e);
} else {
popupAjaxError(e);
}
},
@action
toggleOptionsPanel() {
if (this.showOptions) {
$(".bookmark-options-panel").slideUp("fast");
} else {
$(".bookmark-options-panel").slideDown("fast");
}
this.toggleProperty("showOptions");
},
@action
saveAndClose() {
if (this._saving || this._deleting) {
return;
}
this._saving = true;
this._savingBookmarkManually = true;
return this._saveBookmark()
.then(() => this.send("closeModal"))
.catch((e) => this._handleSaveError(e))
.finally(() => (this._saving = false));
},
@action
delete() {
this._deleting = true;
let deleteAction = () => {
this._closeWithoutSaving = true;
this._deleteBookmark()
.then(() => {
this._deleting = false;
this.send("closeModal");
})
.catch((e) => this._handleSaveError(e));
};
if (this._existingBookmarkHasReminder()) {
bootbox.confirm(I18n.t("bookmarks.confirm_delete"), (result) => {
if (result) {
deleteAction();
}
});
} else {
deleteAction();
}
},
@action
closeWithoutSavingBookmark() {
this._closeWithoutSaving = true;
this.send("closeModal");
},
@action
selectReminderType(type) {
if (type === REMINDER_TYPES.LATER_TODAY && !this.showLaterToday) {
return;
}
this.set("selectedReminderType", type);
if (type !== REMINDER_TYPES.CUSTOM) {
return this.saveAndClose();
}
},
@action
selectPostLocalDate(date) {
this.setProperties({
selectedReminderType: this.reminderTypes.POST_LOCAL_DATE,
postLocalDate: date,
});
return this.saveAndClose();
},
});

View File

@ -12,12 +12,12 @@ export default Controller.extend(ModalFunctionality, {
topicController: inject("topic"),
saving: false,
new_user: null,
newOwner: null,
selectedPostsCount: alias("topicController.selectedPostsCount"),
selectedPostsUsername: alias("topicController.selectedPostsUsername"),
@discourseComputed("saving", "new_user")
@discourseComputed("saving", "newOwner")
buttonDisabled(saving, newUser) {
return saving || isEmpty(newUser);
},
@ -25,7 +25,7 @@ export default Controller.extend(ModalFunctionality, {
onShow() {
this.setProperties({
saving: false,
new_user: "",
newOwner: null,
});
},
@ -35,7 +35,7 @@ export default Controller.extend(ModalFunctionality, {
const options = {
post_ids: this.get("topicController.selectedPostIds"),
username: this.new_user,
username: this.newOwner,
};
Topic.changeOwners(this.get("topicController.model.id"), options).then(
@ -57,5 +57,9 @@ export default Controller.extend(ModalFunctionality, {
return false;
},
updateNewOwner(selected) {
this.set("newOwner", selected.firstObject);
},
},
});

View File

@ -689,7 +689,8 @@ export default Controller.extend({
}
if (currentTopic.id !== composer.get("topic.id")) {
const message = I18n.t("composer.posting_not_on_topic");
const message =
"<h1>" + I18n.t("composer.posting_not_on_topic") + "</h1>";
let buttons = [
{

View File

@ -22,6 +22,7 @@ import { isEmpty } from "@ember/utils";
import { notEmpty } from "@ember/object/computed";
import { setting } from "discourse/lib/computed";
import { userPath } from "discourse/lib/url";
import { helperContext } from "discourse-common/lib/helpers";
export default Controller.extend(
ModalFunctionality,
@ -77,13 +78,26 @@ export default Controller.extend(
return false;
},
@discourseComputed("userFields", "hasAtLeastOneLoginButton")
modalBodyClasses(userFields, hasAtLeastOneLoginButton) {
@discourseComputed()
wavingHandURL() {
const emojiSet = helperContext().siteSettings.emoji_set;
// random number between 2 -6 to render multiple skin tone waving hands
const random = Math.floor(Math.random() * (7 - 2) + 2);
return getURL(`/images/emoji/${emojiSet}/wave/${random}.png`);
},
@discourseComputed(
"userFields",
"hasAtLeastOneLoginButton",
"hasAuthOptions"
)
modalBodyClasses(userFields, hasAtLeastOneLoginButton, hasAuthOptions) {
const classes = [];
if (userFields) {
classes.push("has-user-fields");
}
if (hasAtLeastOneLoginButton) {
if (hasAtLeastOneLoginButton && !hasAuthOptions) {
classes.push("has-alt-auth");
}
return classes.join(" ");

View File

@ -9,6 +9,7 @@ import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
export const CLOSE_STATUS_TYPE = "close";
export const CLOSE_AFTER_LAST_POST_STATUS_TYPE = "close_after_last_post";
export const OPEN_STATUS_TYPE = "open";
export const PUBLISH_TO_CATEGORY_STATUS_TYPE = "publish_to_category";
export const DELETE_STATUS_TYPE = "delete";
@ -28,6 +29,16 @@ export default Controller.extend(ModalFunctionality, {
closed ? "topic.temp_open.title" : "topic.auto_close.title"
),
},
];
if (!closed) {
types.push({
id: CLOSE_AFTER_LAST_POST_STATUS_TYPE,
name: I18n.t("topic.auto_close_after_last_post.title"),
});
}
types.push(
{
id: OPEN_STATUS_TYPE,
name: I18n.t(
@ -41,8 +52,9 @@ export default Controller.extend(ModalFunctionality, {
{
id: BUMP_TYPE,
name: I18n.t("topic.auto_bump.title"),
},
];
}
);
if (this.currentUser.get("staff")) {
types.push(
{
@ -60,34 +72,35 @@ export default Controller.extend(ModalFunctionality, {
topicTimer: alias("model.topic_timer"),
_setTimer(time, duration, statusType, basedOnLastPost, categoryId) {
_setTimer(time, durationMinutes, statusType, basedOnLastPost, categoryId) {
this.set("loading", true);
TopicTimer.updateStatus(
TopicTimer.update(
this.get("model.id"),
time,
basedOnLastPost,
statusType,
categoryId,
duration
durationMinutes
)
.then((result) => {
if (time || duration) {
if (time || durationMinutes) {
this.send("closeModal");
setProperties(this.topicTimer, {
execute_at: result.execute_at,
duration: result.duration,
duration_minutes: result.duration_minutes,
category_id: result.category_id,
});
this.set("model.closed", result.closed);
} else {
this.set("model.topic_timer", EmberObject.create({}));
this.set(
"model.topic_timer",
EmberObject.create({ status_type: this.defaultStatusType })
);
this.setProperties({
selection: null,
});
this.send("onChangeInput", null, null);
}
})
.catch(popupAjaxError)
@ -106,26 +119,45 @@ export default Controller.extend(ModalFunctionality, {
}
}
this.send("onChangeInput", time);
this.send("onChangeInput", null, time);
if (!this.get("topicTimer.status_type")) {
this.send("onChangeStatusType", this.defaultStatusType);
}
if (
this.get("topicTimer.status_type") === CLOSE_STATUS_TYPE &&
this.get("topicTimer.based_on_last_post")
) {
this.send("onChangeStatusType", CLOSE_AFTER_LAST_POST_STATUS_TYPE);
}
},
@discourseComputed("publicTimerTypes")
defaultStatusType(publicTimerTypes) {
return publicTimerTypes[0].id;
},
actions: {
onChangeStatusType(value) {
this.set("topicTimer.status_type", value);
this.setProperties({
"topicTimer.based_on_last_post":
CLOSE_AFTER_LAST_POST_STATUS_TYPE === value,
"topicTimer.status_type": value,
});
},
onChangeInput(value) {
this.set("topicTimer.updateTime", value);
},
onChangeDuration(value) {
this.set("topicTimer.duration", value);
onChangeInput(_type, time) {
if (moment.isMoment(time)) {
time = time.format(FORMAT);
}
this.set("topicTimer.updateTime", time);
},
saveTimer() {
if (
!this.get("topicTimer.updateTime") &&
!this.get("topicTimer.duration")
!this.get("topicTimer.duration_minutes")
) {
this.flash(
I18n.t("topic.topic_status_update.time_frame_required"),
@ -134,10 +166,37 @@ export default Controller.extend(ModalFunctionality, {
return;
}
if (
this.get("topicTimer.duration_minutes") &&
!this.get("topicTimer.updateTime")
) {
if (this.get("topicTimer.duration_minutes") <= 0) {
this.flash(
I18n.t("topic.topic_status_update.min_duration"),
"alert-error"
);
return;
}
// cannot be more than 20 years
if (this.get("topicTimer.duration_minutes") > 20 * 365 * 1440) {
this.flash(
I18n.t("topic.topic_status_update.max_duration"),
"alert-error"
);
return;
}
}
let statusType = this.get("topicTimer.status_type");
if (statusType === CLOSE_AFTER_LAST_POST_STATUS_TYPE) {
statusType = CLOSE_STATUS_TYPE;
}
this._setTimer(
this.get("topicTimer.updateTime"),
this.get("topicTimer.duration"),
this.get("topicTimer.status_type"),
this.get("topicTimer.duration_minutes"),
statusType,
this.get("topicTimer.based_on_last_post"),
this.get("topicTimer.category_id")
);

View File

@ -6,20 +6,18 @@ import discourseComputed from "discourse-common/utils/decorators";
import { emailValid } from "discourse/lib/utilities";
import { extractError } from "discourse/lib/ajax-error";
import { isEmpty } from "@ember/utils";
import { reads } from "@ember/object/computed";
export default Controller.extend(ModalFunctionality, {
loading: false,
setAsOwner: false,
notifyUsers: false,
usernamesAndEmails: null,
usernames: null,
emails: null,
emailsPresent: reads("emails.length"),
onShow() {
this.setProperties({
usernamesAndEmails: "",
usernames: [],
emails: [],
usernamesAndEmails: [],
setAsOwner: false,
notifyUsers: false,
});
@ -30,15 +28,8 @@ export default Controller.extend(ModalFunctionality, {
return loading || !usernamesAndEmails || !(usernamesAndEmails.length > 0);
},
@discourseComputed("usernamesAndEmails")
emailsPresent() {
this._splitEmailsAndUsernames();
return this.emails.length;
},
@discourseComputed("usernamesAndEmails")
notifyUsersDisabled() {
this._splitEmailsAndUsernames();
return this.usernames.length === 0 && this.emails.length > 0;
},
@ -47,6 +38,16 @@ export default Controller.extend(ModalFunctionality, {
return I18n.t("groups.add_members.title", { group_name: fullName || name });
},
@discourseComputed("usernamesAndEmails.[]")
emails(usernamesAndEmails) {
return usernamesAndEmails.filter(emailValid).join(",");
},
@discourseComputed("usernamesAndEmails.[]")
usernames(usernamesAndEmails) {
return usernamesAndEmails.reject(emailValid).join(",");
},
@action
addMembers() {
this.set("loading", true);
@ -89,16 +90,4 @@ export default Controller.extend(ModalFunctionality, {
.catch((error) => this.flash(extractError(error), "error"))
.finally(() => this.set("loading", false));
},
_splitEmailsAndUsernames() {
let emails = [];
let usernames = [];
this.usernamesAndEmails.split(",").forEach((u) => {
emailValid(u) ? emails.push(u) : usernames.push(u);
});
this.set("emails", emails.join(","));
this.set("usernames", usernames.join(","));
},
});

View File

@ -1,6 +1,7 @@
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import discourseDebounce from "discourse-common/lib/debounce";
import { gt } from "@ember/object/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
@ -16,8 +17,11 @@ export default Controller.extend({
filterInput: null,
loading: false,
isBulk: false,
showActions: false,
bulkSelection: null,
@observes("filterInput")
_setFilter() {
discourseDebounce(
@ -51,6 +55,10 @@ export default Controller.extend({
this.model.members.length >= this.model.user_count,
loading: false,
});
if (this.refresh) {
this.set("bulkSelection", []);
}
});
},
@ -97,6 +105,68 @@ export default Controller.extend({
case "removeOwner":
this.removeOwner(member);
break;
case "makePrimary":
member
.setPrimaryGroup(this.model.id)
.then(() => member.set("primary", true));
break;
case "removePrimary":
member.setPrimaryGroup(null).then(() => member.set("primary", false));
break;
}
},
@action
actOnSelection(selection, actionId) {
if (!selection || selection.length === 0) {
return;
}
switch (actionId) {
case "removeMembers":
return ajax(`/groups/${this.model.id}/members.json`, {
type: "DELETE",
data: { user_ids: selection.map((u) => u.id).join(",") },
}).then(() => {
this.model.findMembers(this.memberParams, true);
this.set("isBulk", false);
});
case "makeOwners":
return ajax(`/admin/groups/${this.model.id}/owners.json`, {
type: "PUT",
data: {
group: { usernames: selection.map((u) => u.username).join(",") },
},
}).then(() => {
selection.forEach((s) => s.set("owner", true));
this.set("isBulk", false);
});
case "removeOwners":
return ajax(`/admin/groups/${this.model.id}/owners.json`, {
type: "DELETE",
data: {
group: { usernames: selection.map((u) => u.username).join(",") },
},
}).then(() => {
selection.forEach((s) => s.set("owner", false));
this.set("isBulk", false);
});
case "setPrimary":
case "unsetPrimary":
const primary = actionId === "setPrimary";
return ajax(`/admin/groups/${this.model.id}/primary.json`, {
type: "PUT",
data: {
group: { usernames: selection.map((u) => u.username).join(",") },
primary,
},
}).then(() => {
selection.forEach((s) => s.set("primary", primary));
this.set("isBulk", false);
});
}
},
@ -124,4 +194,33 @@ export default Controller.extend({
.catch(popupAjaxError);
}
},
@action
toggleBulkSelect() {
this.setProperties({
isBulk: !this.isBulk,
bulkSelection: [],
});
},
@action
bulkSelectAll() {
$("input.bulk-select:not(:checked)").click();
},
@action
bulkClearAll() {
$("input.bulk-select:checked").click();
},
@action
selectMember(member, e) {
this.set("bulkSelection", this.bulkSelection || []);
if (e.target.checked) {
this.bulkSelection.pushObject(member);
} else {
this.bulkSelection.removeObject(member);
}
},
});

View File

@ -4,6 +4,7 @@ import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import bootbox from "bootbox";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse-common/utils/decorators";
export function popupAutomaticMembershipAlert(group_id, email_domains) {
if (!email_domains) {
@ -37,6 +38,16 @@ export function popupAutomaticMembershipAlert(group_id, email_domains) {
export default Controller.extend({
saving: null,
@discourseComputed("model.ownerUsernames")
splitOwnerUsernames(owners) {
return owners && owners.length ? owners.split(",") : [];
},
@discourseComputed("model.usernames")
splitUsernames(usernames) {
return usernames && usernames.length ? usernames.split(",") : [];
},
@action
save() {
this.set("saving", true);
@ -55,4 +66,14 @@ export default Controller.extend({
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
},
@action
updateOwnerUsernames(selected) {
this.set("model.ownerUsernames", selected.join(","));
},
@action
updateUsernames(selected) {
this.set("model.usernames", selected.join(","));
},
});

View File

@ -29,5 +29,9 @@ export default Controller.extend(ModalFunctionality, {
.finally(() => this.set("loading", false));
});
},
updateIgnoredUsername(selected) {
this.set("ignoredUsername", selected.firstObject);
},
},
});

View File

@ -1,5 +1,5 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias, or, readOnly } from "@ember/object/computed";
import { alias, not, or, readOnly } from "@ember/object/computed";
import { areCookiesEnabled, escapeExpression } from "discourse/lib/utilities";
import cookie, { removeCookie } from "discourse/lib/cookie";
import { next, schedule } from "@ember/runloop";
@ -18,6 +18,7 @@ import { getWebauthnCredential } from "discourse/lib/webauthn";
import { isEmpty } from "@ember/utils";
import { setting } from "discourse/lib/computed";
import showModal from "discourse/lib/show-modal";
import { helperContext } from "discourse-common/lib/helpers";
// This is happening outside of the app via popup
const AuthErrors = [
@ -45,6 +46,8 @@ export default Controller.extend(ModalFunctionality, {
loginRequired: alias("application.loginRequired"),
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
noLoginLocal: not("canLoginLocal"),
resetForm() {
this.setProperties({
loggingIn: false,
@ -62,20 +65,47 @@ export default Controller.extend(ModalFunctionality, {
return showSecondFactor || showSecurityKey ? "hidden" : "";
},
@discourseComputed()
wavingHandURL() {
const emojiSet = helperContext().siteSettings.emoji_set;
// random number between 2 -6 to render multiple skin tone waving hands
const random = Math.floor(Math.random() * (7 - 2) + 2);
return getURL(`/images/emoji/${emojiSet}/wave/${random}.png`);
},
@discourseComputed("showSecondFactor", "showSecurityKey")
secondFactorClass(showSecondFactor, showSecurityKey) {
return showSecondFactor || showSecurityKey ? "" : "hidden";
},
@discourseComputed("awaitingApproval", "hasAtLeastOneLoginButton")
modalBodyClasses(awaitingApproval, hasAtLeastOneLoginButton) {
@discourseComputed(
"awaitingApproval",
"hasAtLeastOneLoginButton",
"showSecondFactor",
"canLoginLocal",
"showSecurityKey"
)
modalBodyClasses(
awaitingApproval,
hasAtLeastOneLoginButton,
showSecondFactor,
canLoginLocal,
showSecurityKey
) {
const classes = ["login-modal"];
if (awaitingApproval) {
classes.push("awaiting-approval");
}
if (hasAtLeastOneLoginButton) {
if (hasAtLeastOneLoginButton && !showSecondFactor && !showSecurityKey) {
classes.push("has-alt-auth");
}
if (!canLoginLocal) {
classes.push("no-local-login");
}
if (showSecondFactor || showSecurityKey) {
classes.push("second-factor");
}
return classes.join(" ");
},
@ -141,8 +171,6 @@ export default Controller.extend(ModalFunctionality, {
(result.security_key_enabled || result.totp_enabled) &&
!this.secondFactorRequired
) {
document.getElementById("modal-alert").style.display = "none";
this.setProperties({
otherMethodAllowed: result.multiple_second_factor_methods,
secondFactorRequired: true,
@ -282,7 +310,7 @@ export default Controller.extend(ModalFunctionality, {
}
if (isEmpty(this.loginName)) {
this.flash(I18n.t("login.blank_username"), "error");
this.flash(I18n.t("login.blank_username"), "info");
return;
}
@ -392,7 +420,7 @@ export default Controller.extend(ModalFunctionality, {
}
const skipConfirmation =
options && this.siteSettings.external_auth_skip_create_confirm;
options && this.siteSettings.auth_skip_create_confirm;
const createAccountController = this.createAccount;
createAccountController.setProperties({

View File

@ -80,7 +80,7 @@ export default Controller.extend(ModalFunctionality, {
categoryId: null,
topicName: "",
tags: null,
participants: null,
participants: [],
});
const isPrivateMessage = this.get("model.isPrivateMessage");
@ -133,7 +133,7 @@ export default Controller.extend(ModalFunctionality, {
} else if (type === "existingMessage") {
mergeOptions = {
destination_topic_id: this.selectedTopicId,
participants: this.participants,
participants: this.participants.join(","),
archetype: "private_message",
};
moveOptions = Object.assign(

View File

@ -71,7 +71,8 @@ export default Controller.extend(CanCheckEmails, {
return false;
} else {
return (
!this.siteSettings.enable_sso && this.siteSettings.enable_local_logins
!this.siteSettings.enable_discourse_connect &&
this.siteSettings.enable_local_logins
);
}
},

View File

@ -131,7 +131,8 @@ export default Controller.extend(CanCheckEmails, {
link: true,
},
{
label: `${iconHTML("ban")}${I18n.t("user.second_factor.disable")}`,
icon: iconHTML("ban"),
label: I18n.t("user.second_factor.disable"),
class: "btn-danger btn-icon-text",
callback: () => {
this.model

View File

@ -122,6 +122,9 @@ export default Controller.extend(ModalFunctionality, StateHelpers, {
@action
onChangePublic(isPublic) {
this.publishedPage.set("public", isPublic);
this.publish();
if (this.showUnpublish) {
this.publish();
}
},
});

View File

@ -1,4 +1,5 @@
import discourseComputed, { on } from "discourse-common/utils/decorators";
import BufferedMixin from "ember-buffered-proxy/mixin";
import BufferedProxy from "ember-buffered-proxy/proxy";
import Controller from "@ember/controller";
import EmberObjectProxy from "@ember/object/proxy";
@ -11,14 +12,13 @@ import { sort } from "@ember/object/computed";
export default Controller.extend(ModalFunctionality, Evented, {
init() {
this._super(...arguments);
this.categoriesSorting = ["position"];
},
@discourseComputed("site.categories.[]")
categoriesBuffered(categories) {
const bufProxy = EmberObjectProxy.extend(BufferedProxy);
return categories.map((c) => bufProxy.create({ content: c }));
const bufProxy = EmberObjectProxy.extend(BufferedMixin || BufferedProxy);
return (categories || []).map((c) => bufProxy.create({ content: c }));
},
categoriesOrdered: sort("categoriesBuffered", "categoriesSorting"),
@ -135,7 +135,7 @@ export default Controller.extend(ModalFunctionality, Evented, {
type: "POST",
data: { mapping: JSON.stringify(data) },
})
.then(() => this.send("closeModal"))
.then(() => window.location.reload())
.catch(popupAjaxError);
},
},

View File

@ -170,5 +170,13 @@ export default Controller.extend({
toggleFilters() {
this.toggleProperty("filtersExpanded");
},
updateFilterReviewedBy(selected) {
this.set("filterReviewedBy", selected.firstObject);
},
updateFilterUsername(selected) {
this.set("filterUsername", selected.firstObject);
},
},
});

View File

@ -1,13 +1,14 @@
import Controller, { inject as controller } from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import { equal, or } from "@ember/object/computed";
import { userPath } from "discourse/lib/url";
export default Controller.extend({
application: controller(),
showLoginButton: equal("model.path", "login"),
anyButtons: or("showLoginButton", "showSignupButton"),
@discourseComputed("model.path")
bodyClass: (path) => `static-${path}`,

View File

@ -4,6 +4,7 @@ import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
import FilterModeMixin from "discourse/mixins/filter-mode";
import I18n from "I18n";
import NavItem from "discourse/models/nav-item";
import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed";
import bootbox from "bootbox";
import { queryParams } from "discourse/controllers/discovery-sortable";
@ -109,11 +110,34 @@ export default Controller.extend(BulkTopicSelection, FilterModeMixin, {
return this.isFilterPage(filter, "unread") && topicsLength > 0;
},
@discourseComputed("list.filter", "list.topics.length")
showResetNew(filter, topicsLength) {
return this.isFilterPage(filter, "new") && topicsLength > 0;
},
actions: {
dismissReadPosts() {
showModal("dismiss-read", { title: "topics.bulk.dismiss_read" });
},
resetNew() {
const tracked =
(this.router.currentRoute.queryParams["f"] ||
this.router.currentRoute.queryParams["filter"]) === "tracked";
Topic.resetNew(
this.category,
!this.noSubcategories,
tracked,
this.tag
).then(() =>
this.send(
"refresh",
tracked ? { skipResettingParams: ["filter", "f"] } : {}
)
);
},
changeSort(order) {
if (order === this.order) {
this.toggleProperty("ascending");

View File

@ -71,8 +71,10 @@ export default Controller.extend({
return;
}
const joinedTags = tags.slice(0, displayN).join(", ");
let more = Math.max(0, tags.length - displayN);
const joinedTags = tags
.slice(0, displayN)
.join(I18n.t("tagging.tag_list_joiner"));
const more = Math.max(0, tags.length - displayN);
const tagsString =
more === 0

View File

@ -255,7 +255,16 @@ export default Controller.extend(ModalFunctionality, {
},
removeTags() {
this.performAndRefresh({ type: "remove_tags" });
bootbox.confirm(
I18n.t("topics.bulk.confirm_remove_tags", {
count: this.get("model.topics").length,
}),
(result) => {
if (result) {
this.performAndRefresh({ type: "remove_tags" });
}
}
);
},
},
});

View File

@ -67,6 +67,7 @@ export default Controller.extend(bufferedProperty("model"), {
replies_to_post_number: null,
filter: null,
quoteState: null,
currentPostId: null,
init() {
this._super(...arguments);
@ -338,7 +339,7 @@ export default Controller.extend(bufferedProperty("model"), {
this.appEvents.trigger("composer:insert-block", quotedText);
} else if (composer.get("model.viewDraft")) {
const model = composer.get("model");
model.set("reply", model.get("reply") + quotedText);
model.set("reply", model.get("reply") + "\n" + quotedText);
composer.send("openIfDraft");
} else {
composer.open(composerOpts);
@ -360,6 +361,7 @@ export default Controller.extend(bufferedProperty("model"), {
return;
}
this.set("currentPostId", post.id);
const postNumber = post.get("post_number");
const topic = this.model;
topic.set("currentPost", postNumber);
@ -429,19 +431,28 @@ export default Controller.extend(bufferedProperty("model"), {
});
},
cancelFilter(previousFilters) {
this.get("model.postStream").cancelFilter();
this.get("model.postStream")
.refresh()
cancelFilter(nearestPost = null) {
const postStream = this.get("model.postStream");
if (!nearestPost) {
const loadedPost = postStream.findLoadedPost(this.currentPostId);
if (loadedPost) {
nearestPost = loadedPost.post_number;
} else {
postStream.findPostsByIds([this.currentPostId]).then((arr) => {
nearestPost = arr[0].post_number;
});
}
}
postStream.cancelFilter();
postStream
.refresh({
nearPost: nearestPost,
forceLoad: true,
})
.then(() => {
if (previousFilters) {
if (previousFilters.replies_to_post_number) {
this._jumpToPostNumber(previousFilters.replies_to_post_number);
}
if (previousFilters.filter_upwards_post_id) {
this._jumpToPostId(previousFilters.filter_upwards_post_id);
}
}
DiscourseURL.routeTo(this.model.urlForPostNumber(nearestPost));
this.updateQueryParams();
});
},
@ -1088,13 +1099,7 @@ export default Controller.extend(bufferedProperty("model"), {
},
removeTopicTimer(statusType, topicTimer) {
TopicTimer.updateStatus(
this.get("model.id"),
null,
null,
statusType,
null
)
TopicTimer.update(this.get("model.id"), null, null, statusType, null)
.then(() => this.set(`model.${topicTimer}`, EmberObject.create({})))
.catch((error) => popupAjaxError(error));
},

View File

@ -56,7 +56,7 @@ export default Controller.extend(ModalFunctionality, {
const imageUrl = this.imageUrl || "";
const toolbarEvent = this.toolbarEvent;
if (imageUrl.match(/\.(jpg|jpeg|png|gif|heic|heif)$/)) {
if (imageUrl.match(/\.(jpg|jpeg|png|gif|heic|heif|webp)$/)) {
toolbarEvent.addText(`![](${imageUrl})`);
} else {
toolbarEvent.addText(imageUrl);

View File

@ -1,6 +1,6 @@
import Controller, { inject } from "@ember/controller";
import EmberObject, { computed, set } from "@ember/object";
import { alias, and, gt, not, or } from "@ember/object/computed";
import { alias, and, equal, gt, not, or } from "@ember/object/computed";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import User from "discourse/models/user";
import I18n from "I18n";
@ -14,7 +14,6 @@ import { prioritizeNameInUx } from "discourse/lib/settings";
import { inject as service } from "@ember/service";
export default Controller.extend(CanCheckEmails, {
indexStream: false,
router: service(),
userNotifications: inject("user-notifications"),
currentPath: alias("router._router.currentPath"),
@ -36,17 +35,19 @@ export default Controller.extend(CanCheckEmails, {
return !isEmpty(background.toString());
},
isSummaryRoute: equal("router.currentRouteName", "user.summary"),
@discourseComputed(
"model.profile_hidden",
"indexStream",
"isSummaryRoute",
"viewingSelf",
"forceExpand"
)
collapsedInfo(profileHidden, indexStream, viewingSelf, forceExpand) {
collapsedInfo(profileHidden, isSummaryRoute, viewingSelf, forceExpand) {
if (profileHidden) {
return true;
}
return (!indexStream || viewingSelf) && !forceExpand;
return (!isSummaryRoute || viewingSelf) && !forceExpand;
},
canMuteOrIgnoreUser: or("model.can_ignore_user", "model.can_mute_user"),
hasGivenFlags: gt("model.number_of_flags_given", 0),
@ -56,6 +57,15 @@ export default Controller.extend(CanCheckEmails, {
hasReceivedWarnings: gt("model.warnings_received_count", 0),
hasRejectedPosts: gt("model.number_of_rejected_posts", 0),
collapsedInfoState: computed("collapsedInfo", function () {
return {
isExpanded: !this.collapsedInfo,
icon: this.collapsedInfo ? "angle-double-down" : "angle-double-up",
label: this.collapsedInfo ? "expand_profile" : "collapse_profile",
action: this.collapsedInfo ? "expandProfile" : "collapseProfile",
};
}),
showStaffCounters: or(
"hasGivenFlags",
"hasFlaggedPosts",
@ -92,6 +102,11 @@ export default Controller.extend(CanCheckEmails, {
return viewingSelf;
},
@discourseComputed("viewingSelf")
showRead(viewingSelf) {
return viewingSelf;
},
@discourseComputed("viewingSelf", "currentUser.admin")
showPrivateMessages(viewingSelf, isAdmin) {
return (

View File

@ -1,6 +1,7 @@
import { helperContext, registerUnbound } from "discourse-common/lib/helpers";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { htmlSafe } from "@ember/template";
import { RUNTIME_OPTIONS } from "discourse-common/lib/raw-handlebars-helpers";
function renderRaw(ctx, template, templateName, params) {
params = jQuery.extend({}, params);
@ -21,7 +22,7 @@ function renderRaw(ctx, template, templateName, params) {
}
}
return htmlSafe(template(params));
return htmlSafe(template(params, RUNTIME_OPTIONS));
}
registerUnbound("raw", function (templateName, params) {

View File

@ -1,4 +1,5 @@
import { avatarImg, formatUsername } from "discourse/lib/utilities";
import I18n from "I18n";
import { get } from "@ember/object";
import { htmlSafe } from "@ember/template";
import { prioritizeNameInUx } from "discourse/lib/settings";
@ -59,7 +60,10 @@ function renderAvatar(user, options) {
// if a description has been provided
if (description && description.length > 0) {
// preprend the username before the description
title = displayName + " - " + description;
title = I18n.t("user.avatar.name_and_description", {
name: displayName,
description,
});
}
}
}

View File

@ -0,0 +1,11 @@
import { registerUnbound } from "discourse-common/lib/helpers";
registerUnbound("value-entered", function (value) {
if (!value) {
return "";
} else if (value.length > 0) {
return "value-entered";
} else {
return "";
}
});

View File

@ -63,6 +63,8 @@ export default {
name: "copy-codeblocks",
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
withPluginApi("0.8.7", (api) => {
function _cleanUp() {
Object.values(_copyCodeblocksClickHandlers || {}).forEach((handler) =>
@ -112,7 +114,6 @@ export default {
return;
}
const siteSettings = container.lookup("site-settings:main");
if (!siteSettings.show_copy_button_on_codeblocks) {
return;
}

View File

@ -3,7 +3,7 @@ import bootbox from "bootbox";
export default {
name: "jquery-plugins",
initialize: function () {
initialize() {
// Settings for bootbox
bootbox.animate(false);
bootbox.backdrop(true);

View File

@ -318,6 +318,14 @@ export default function (options) {
return createPopper(me[0], div[0], {
placement: "bottom-start",
strategy: "fixed",
modifiers: [
{
name: "offset",
options: {
offset: [0, 2],
},
},
],
});
}

View File

@ -16,17 +16,3 @@ export function formattedReminderTime(reminderAt, timezone) {
date_time: reminderAtDate.format(I18n.t("dates.long_with_year")),
});
}
export const REMINDER_TYPES = {
LATER_TODAY: "later_today",
NEXT_BUSINESS_DAY: "next_business_day",
TOMORROW: "tomorrow",
NEXT_WEEK: "next_week",
NEXT_MONTH: "next_month",
CUSTOM: "custom",
LAST_CUSTOM: "last_custom",
NONE: "none",
START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week",
LATER_THIS_WEEK: "later_this_week",
POST_LOCAL_DATE: "post_local_date",
};

View File

@ -1,3 +1,4 @@
import { isAppWebview } from "discourse/lib/utilities";
import { later, run, schedule, throttle } from "@ember/runloop";
import {
nextTopicUrl,
@ -22,6 +23,10 @@ const DEFAULT_BINDINGS = {
C: { handler: "focusComposer" },
"ctrl+f": { handler: "showPageSearch", anonymous: true },
"command+f": { handler: "showPageSearch", anonymous: true },
"command+left": { handler: "webviewKeyboardBack" },
"command+[": { handler: "webviewKeyboardBack" },
"command+right": { handler: "webviewKeyboardForward" },
"command+]": { handler: "webviewKeyboardForward" },
"mod+p": { handler: "printTopic", anonymous: true },
d: { postAction: "deletePost" },
e: { postAction: "editPost" },
@ -124,7 +129,15 @@ export default {
this.container = null;
},
isTornDown() {
return this.keyTrapper == null || this.container == null;
},
bindKey(key, binding = null) {
if (this.isTornDown()) {
return;
}
if (!binding) {
binding = DEFAULT_BINDINGS[key];
}
@ -152,11 +165,21 @@ export default {
// for cases when you want to disable global keyboard shortcuts
// so that you can override them (e.g. inside a modal)
pause(combinations) {
if (this.isTornDown()) {
return;
}
combinations.forEach((combo) => this.keyTrapper.unbind(combo));
},
// restore global shortcuts that you have paused
unpause(combinations) {
if (this.isTornDown()) {
return;
}
// if the keytrapper has already been torn down this will error
if (this.keyTrapper == null) {
return;
}
combinations.forEach((combo) => this.bindKey(combo));
},
@ -770,4 +793,16 @@ export default {
toggleAdminActions() {
this.appEvents.trigger("topic:toggle-actions");
},
webviewKeyboardBack() {
if (isAppWebview()) {
window.history.back();
}
},
webviewKeyboardForward() {
if (isAppWebview()) {
window.history.forward();
}
},
};

View File

@ -1,7 +1,6 @@
import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
import { ajax } from "discourse/lib/ajax";
import { replaceSpan } from "discourse/lib/category-hashtags";
import { schedule } from "@ember/runloop";
const categoryHashtags = {};
const tagHashtags = {};
@ -15,21 +14,19 @@ export function linkSeenHashtags($elem) {
const slugs = [...$hashtags.map((_, hashtag) => hashtag.innerText.substr(1))];
schedule("afterRender", () => {
$hashtags.each((index, hashtag) => {
let slug = slugs[index];
const hasTagSuffix = slug.endsWith(TAG_HASHTAG_POSTFIX);
if (hasTagSuffix) {
slug = slug.substr(0, slug.length - TAG_HASHTAG_POSTFIX.length);
}
$hashtags.each((index, hashtag) => {
let slug = slugs[index];
const hasTagSuffix = slug.endsWith(TAG_HASHTAG_POSTFIX);
if (hasTagSuffix) {
slug = slug.substr(0, slug.length - TAG_HASHTAG_POSTFIX.length);
}
const lowerSlug = slug.toLowerCase();
if (categoryHashtags[lowerSlug] && !hasTagSuffix) {
replaceSpan($(hashtag), slug, categoryHashtags[lowerSlug]);
} else if (tagHashtags[lowerSlug]) {
replaceSpan($(hashtag), slug, tagHashtags[lowerSlug]);
}
});
const lowerSlug = slug.toLowerCase();
if (categoryHashtags[lowerSlug] && !hasTagSuffix) {
replaceSpan($(hashtag), slug, categoryHashtags[lowerSlug]);
} else if (tagHashtags[lowerSlug]) {
replaceSpan($(hashtag), slug, tagHashtags[lowerSlug]);
}
});
return slugs

View File

@ -1,7 +1,6 @@
import { ajax } from "discourse/lib/ajax";
import { formatUsername } from "discourse/lib/utilities";
import getURL from "discourse-common/lib/get-url";
import { schedule } from "@ember/runloop";
import { userPath } from "discourse/lib/url";
let maxGroupMention;
@ -42,23 +41,21 @@ const checked = {};
const cannotSee = [];
function updateFound($mentions, usernames) {
schedule("afterRender", function () {
$mentions.each((i, e) => {
const $e = $(e);
const username = usernames[i];
if (found[username.toLowerCase()]) {
replaceSpan($e, username, { cannot_see: cannotSee[username] });
} else if (mentionableGroups[username]) {
replaceSpan($e, username, {
group: true,
mentionable: mentionableGroups[username],
});
} else if (foundGroups[username]) {
replaceSpan($e, username, { group: true });
} else if (checked[username]) {
$e.addClass("mention-tested");
}
});
$mentions.each((i, e) => {
const $e = $(e);
const username = usernames[i];
if (found[username.toLowerCase()]) {
replaceSpan($e, username, { cannot_see: cannotSee[username] });
} else if (mentionableGroups[username]) {
replaceSpan($e, username, {
group: true,
mentionable: mentionableGroups[username],
});
} else if (foundGroups[username]) {
replaceSpan($e, username, { group: true });
} else if (checked[username]) {
$e.addClass("mention-tested");
}
});
}

View File

@ -7,7 +7,8 @@ export function loadOneboxes(
topicId,
categoryId,
maxOneboxes,
refresh
refresh,
offline
) {
const oneboxes = {};
const inlineOneboxes = {};
@ -41,17 +42,22 @@ export function loadOneboxes(
}
});
let newBoxes = 0;
if (Object.keys(oneboxes).length > 0) {
_loadOneboxes(oneboxes, ajax, newBoxes, topicId, categoryId, refresh);
_loadOneboxes({
oneboxes,
ajax,
topicId,
categoryId,
refresh,
offline,
});
}
if (Object.keys(inlineOneboxes).length > 0) {
_loadInlineOneboxes(inlineOneboxes, ajax, topicId, categoryId);
}
return newBoxes;
return Object.keys(oneboxes).length + Object.keys(inlineOneboxes).length;
}
function _loadInlineOneboxes(inline, ajax, topicId, categoryId) {
@ -61,18 +67,24 @@ function _loadInlineOneboxes(inline, ajax, topicId, categoryId) {
});
}
function _loadOneboxes(oneboxes, ajax, count, topicId, categoryId, refresh) {
function _loadOneboxes({
oneboxes,
ajax,
topicId,
categoryId,
refresh,
offline,
}) {
Object.values(oneboxes).forEach((onebox) => {
onebox.forEach((o) => {
onebox.forEach((elem) => {
load({
elem: o,
refresh,
elem,
ajax,
categoryId: categoryId,
topicId: topicId,
categoryId,
topicId,
refresh,
offline,
});
count++;
});
});
}

View File

@ -1223,8 +1223,6 @@ class PluginApi {
}
}
let _pluginv01;
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
function cmpVersions(a, b) {
let i, diff;
@ -1244,16 +1242,24 @@ function cmpVersions(a, b) {
function getPluginApi(version) {
version = version.toString();
if (cmpVersions(version, PLUGIN_API_VERSION) <= 0) {
if (!_pluginv01) {
_pluginv01 = new PluginApi(version, getOwner(this));
const owner = getOwner(this);
let pluginApi = owner.lookup("plugin-api:main");
if (!pluginApi) {
pluginApi = new PluginApi(version, owner);
owner.registry.register("plugin-api:main", pluginApi, {
instantiate: false,
});
}
// We are recycling the compatible object, but let's update to the higher version
if (_pluginv01.version < version) {
_pluginv01.version = version;
if (pluginApi.version < version) {
pluginApi.version = version;
}
return _pluginv01;
return pluginApi;
} else {
// eslint-disable-next-line no-console
console.warn(`Plugin API v${version} is not supported`);
@ -1306,7 +1312,3 @@ function decorate(klass, evt, cb, id) {
});
klass.reopen(mixin);
}
export function resetPluginApi() {
_pluginv01 = null;
}

View File

@ -6,6 +6,7 @@ export const PUBLIC_JS_VERSIONS = {
"Chart.min.js": "chart.js/2.9.3/Chart.min.js",
"chartjs-plugin-datalabels.min.js":
"chartjs-plugin-datalabels/0.7.0/chartjs-plugin-datalabels.min.js",
"diffhtml.min.js": "diffhtml/1.0.0-beta.18/diffhtml.min.js",
"jquery.magnific-popup.min.js":
"magnific-popup/1.1.0/jquery.magnific-popup.min.js",
"pikaday.js": "pikaday/1.8.0/pikaday.js",

View File

@ -0,0 +1,112 @@
import {
MOMENT_MONDAY,
laterThisWeek,
laterToday,
nextBusinessWeekStart,
nextMonth,
nextWeek,
now,
tomorrow,
} from "discourse/lib/time-utils";
import I18n from "I18n";
export const TIME_SHORTCUT_TYPES = {
LATER_TODAY: "later_today",
TOMORROW: "tomorrow",
NEXT_WEEK: "next_week",
NEXT_MONTH: "next_month",
CUSTOM: "custom",
RELATIVE: "relative",
LAST_CUSTOM: "last_custom",
NONE: "none",
START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week",
LATER_THIS_WEEK: "later_this_week",
POST_LOCAL_DATE: "post_local_date",
};
export function defaultShortcutOptions(timezone) {
return [
{
icon: "angle-right",
id: TIME_SHORTCUT_TYPES.LATER_TODAY,
label: "time_shortcut.later_today",
time: laterToday(timezone),
timeFormatted: laterToday(timezone).format(I18n.t("dates.time")),
hidden: true,
},
{
icon: "far-sun",
id: TIME_SHORTCUT_TYPES.TOMORROW,
label: "time_shortcut.tomorrow",
time: tomorrow(timezone),
timeFormatted: tomorrow(timezone).format(I18n.t("dates.time_short_day")),
},
{
icon: "angle-double-right",
id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK,
label: "time_shortcut.later_this_week",
time: laterThisWeek(timezone),
timeFormatted: laterThisWeek(timezone).format(
I18n.t("dates.time_short_day")
),
hidden: true,
},
{
icon: "briefcase",
id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK,
label:
now(timezone).day() === MOMENT_MONDAY
? "time_shortcut.start_of_next_business_week_alt"
: "time_shortcut.start_of_next_business_week",
time: nextBusinessWeekStart(timezone),
timeFormatted: nextBusinessWeekStart(timezone).format(
I18n.t("dates.long_no_year")
),
},
{
icon: "far-clock",
id: TIME_SHORTCUT_TYPES.NEXT_WEEK,
label: "time_shortcut.next_week",
time: nextWeek(timezone),
timeFormatted: nextWeek(timezone).format(I18n.t("dates.long_no_year")),
},
{
icon: "far-calendar-plus",
id: TIME_SHORTCUT_TYPES.NEXT_MONTH,
label: "time_shortcut.next_month",
time: nextMonth(timezone),
timeFormatted: nextMonth(timezone).format(I18n.t("dates.long_no_year")),
},
{
icon: "far-clock",
id: TIME_SHORTCUT_TYPES.RELATIVE,
label: "time_shortcut.relative",
time: null,
timeFormatted: null,
isRelativeTimeShortcut: true,
},
{
icon: "calendar-alt",
id: TIME_SHORTCUT_TYPES.CUSTOM,
label: "time_shortcut.custom",
time: null,
timeFormatted: null,
isCustomTimeShortcut: true,
},
{
icon: "undo",
id: TIME_SHORTCUT_TYPES.LAST_CUSTOM,
label: "time_shortcut.last_custom",
time: null,
timeFormatted: null,
hidden: true,
},
{
icon: "ban",
id: TIME_SHORTCUT_TYPES.NONE,
label: "time_shortcut.none",
time: null,
timeFormatted: null,
},
];
}

View File

@ -0,0 +1,68 @@
import { isPresent } from "@ember/utils";
export const START_OF_DAY_HOUR = 8;
export const LATER_TODAY_CUTOFF_HOUR = 17;
export const LATER_TODAY_MAX_HOUR = 18;
export const MOMENT_MONDAY = 1;
export const MOMENT_THURSDAY = 4;
export const MOMENT_SATURDAY = 6;
export function now(timezone) {
return moment.tz(timezone);
}
export function startOfDay(momentDate, startOfDayHour = START_OF_DAY_HOUR) {
return momentDate.hour(startOfDayHour).startOf("hour");
}
export function tomorrow(timezone) {
return startOfDay(now(timezone).add(1, "day"));
}
export function thisWeekend(timezone) {
return startOfDay(now(timezone).day(MOMENT_SATURDAY));
}
export function laterToday(timezone) {
let later = now(timezone).add(3, "hours");
if (later.hour() >= LATER_TODAY_MAX_HOUR) {
return later.hour(LATER_TODAY_MAX_HOUR).startOf("hour");
}
return later.minutes() < 30
? later.startOf("hour")
: later.add(30, "minutes").startOf("hour");
}
export function laterThisWeek(timezone) {
return startOfDay(now(timezone).add(2, "days"));
}
export function nextWeek(timezone) {
return startOfDay(now(timezone).add(7, "days"));
}
export function nextMonth(timezone) {
return startOfDay(now(timezone).add(1, "month"));
}
export function nextBusinessWeekStart(timezone) {
return nextWeek(timezone).day(MOMENT_MONDAY);
}
export function parseCustomDatetime(
date,
time,
currentTimezone,
parseTimezone = null
) {
let dateTime = isPresent(time) ? `${date} ${time}` : date;
parseTimezone = parseTimezone || currentTimezone;
let parsed = moment.tz(dateTime, parseTimezone);
if (parseTimezone !== currentTimezone) {
parsed = parsed.tz(currentTimezone);
}
return parsed;
}

View File

@ -113,7 +113,18 @@ export class Tag {
}
static allowedTags() {
return ["ins", "del", "small", "big", "kbd", "ruby", "rt", "rb", "rp"];
return [
"ins",
"del",
"small",
"big",
"kbd",
"ruby",
"rt",
"rb",
"rp",
"mark",
];
}
static block(name, prefix, suffix) {

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