Version bump
This commit is contained in:
commit
33df4233c9
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@ -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
7
.gitignore
vendored
@ -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
|
||||
|
||||
2
Gemfile
2
Gemfile
@ -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'
|
||||
|
||||
|
||||
57
Gemfile.lock
57
Gemfile.lock
@ -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
|
||||
|
||||
@ -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));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{{tag-chooser
|
||||
tags=selectedTags
|
||||
onChange=(action "changeSelectedTags")
|
||||
everyTag=true
|
||||
options=(hash
|
||||
allowAny=false
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -21,4 +21,4 @@
|
||||
label="admin.user_fields.create"
|
||||
icon="plus"}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -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}}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
},
|
||||
});
|
||||
422
app/assets/javascripts/discourse/app/components/bookmark.js
Normal file
422
app/assets/javascripts/discourse/app/components/bookmark.js
Normal 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();
|
||||
},
|
||||
});
|
||||
@ -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";
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 +=
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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(","));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}),
|
||||
});
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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("");
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -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()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}`;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = "";
|
||||
|
||||
@ -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
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
|
||||
@ -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(" ");
|
||||
|
||||
@ -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")
|
||||
);
|
||||
|
||||
@ -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(","));
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(","));
|
||||
},
|
||||
});
|
||||
|
||||
@ -29,5 +29,9 @@ export default Controller.extend(ModalFunctionality, {
|
||||
.finally(() => this.set("loading", false));
|
||||
});
|
||||
},
|
||||
|
||||
updateIgnoredUsername(selected) {
|
||||
this.set("ignoredUsername", selected.firstObject);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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));
|
||||
},
|
||||
|
||||
@ -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(``);
|
||||
} else {
|
||||
toolbarEvent.addText(imageUrl);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
};
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
112
app/assets/javascripts/discourse/app/lib/time-shortcut.js
Normal file
112
app/assets/javascripts/discourse/app/lib/time-shortcut.js
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
||||
68
app/assets/javascripts/discourse/app/lib/time-utils.js
Normal file
68
app/assets/javascripts/discourse/app/lib/time-utils.js
Normal 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;
|
||||
}
|
||||
@ -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
Reference in New Issue
Block a user