Version bump
This commit is contained in:
commit
8b8dee956c
@ -2,10 +2,11 @@ skip_missing_workers: true
|
||||
allow_lossy: false
|
||||
# PNG
|
||||
advpng: false
|
||||
optipng:
|
||||
optipng:
|
||||
level: 2
|
||||
pngcrush: false
|
||||
pngout: false
|
||||
pngquant: false
|
||||
# JPG
|
||||
jpegrecompress: false
|
||||
timeout: 15
|
||||
|
||||
13
.travis.yml
13
.travis.yml
@ -5,8 +5,10 @@ env:
|
||||
- DISCOURSE_HOSTNAME=www.example.com
|
||||
- RUBY_GC_MALLOC_LIMIT=50000000
|
||||
matrix:
|
||||
- "RAILS_MASTER=0"
|
||||
- "RAILS_MASTER=1"
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=0"
|
||||
- "RAILS_MASTER=1 QUNIT_RUN=0"
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=1"
|
||||
- "RAILS_MASTER=1 QUNIT_RUN=1"
|
||||
|
||||
addons:
|
||||
postgresql: 9.5
|
||||
@ -19,11 +21,12 @@ addons:
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: "RAILS_MASTER=1"
|
||||
- rvm: rbx-2
|
||||
- env: "RAILS_MASTER=1 QUNIT_RUN=0"
|
||||
- env: "RAILS_MASTER=1 QUNIT_RUN=1"
|
||||
fast_finish: true
|
||||
|
||||
rvm:
|
||||
- 2.4.1
|
||||
- 2.3.3
|
||||
|
||||
services:
|
||||
@ -57,4 +60,4 @@ install:
|
||||
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
|
||||
|
||||
script: "bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test['200000']"
|
||||
script: bash -c "if [ '$QUNIT_RUN' == '0' ]; then bundle exec rspec && bundle exec rake plugin:spec; else bundle exec rake qunit:test['200000']; fi"
|
||||
|
||||
25
Gemfile
25
Gemfile
@ -2,6 +2,9 @@ source 'https://rubygems.org'
|
||||
# if there is a super emergency and rubygems is playing up, try
|
||||
#source 'http://production.cf.rubygems.org'
|
||||
|
||||
# does not install in linux ATM, so hack this for now
|
||||
# gem 'bootsnap', require: false
|
||||
|
||||
def rails_master?
|
||||
ENV["RAILS_MASTER"] == '1'
|
||||
end
|
||||
@ -66,10 +69,9 @@ gem 'unf', require: false
|
||||
|
||||
gem 'email_reply_trimmer', '0.1.6'
|
||||
|
||||
# note: for image_optim to correctly work you need to follow
|
||||
# https://github.com/toy/image_optim
|
||||
# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade
|
||||
gem 'image_optim', '0.20.2'
|
||||
# TODO Use official image_optim gem once https://github.com/toy/image_optim/pull/149
|
||||
# is merged.
|
||||
gem 'discourse_image_optim', require: 'image_optim'
|
||||
gem 'multi_json'
|
||||
gem 'mustache'
|
||||
gem 'nokogiri'
|
||||
@ -96,10 +98,7 @@ gem 'thor', require: false
|
||||
gem 'rest-client'
|
||||
gem 'rinku'
|
||||
gem 'sanitize'
|
||||
gem 'sass'
|
||||
gem 'sass-rails'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-statistic'
|
||||
|
||||
# for sidekiq web
|
||||
gem 'sinatra', require: false
|
||||
@ -117,6 +116,7 @@ group :assets do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'webmock', require: false
|
||||
gem 'fakeweb', '~> 1.3.0', require: false
|
||||
gem 'minitest', require: false
|
||||
gem 'timecop'
|
||||
@ -127,7 +127,7 @@ end
|
||||
group :test, :development do
|
||||
gem 'rspec'
|
||||
gem 'mock_redis'
|
||||
gem 'listen', '0.7.3', require: false
|
||||
gem 'listen', require: false
|
||||
gem 'certified', require: false
|
||||
# later appears to break Fabricate(:topic, category: category)
|
||||
gem 'fabrication', '2.9.8', require: false
|
||||
@ -184,3 +184,12 @@ gem 'memory_profiler', require: false, platform: :mri
|
||||
gem 'rmmseg-cpp', require: false
|
||||
|
||||
gem 'logster'
|
||||
|
||||
gem 'sassc', require: false
|
||||
|
||||
|
||||
if ENV["IMPORT"] == "1"
|
||||
gem 'mysql2'
|
||||
gem 'php_serialize'
|
||||
gem 'redcarpet'
|
||||
end
|
||||
|
||||
178
Gemfile.lock
178
Gemfile.lock
@ -1,47 +1,48 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activejob (= 4.2.7.1)
|
||||
actionmailer (4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
actionview (= 4.2.8)
|
||||
activejob (= 4.2.8)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
actionpack (4.2.8)
|
||||
actionview (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
actionview (4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_model_serializers (0.8.3)
|
||||
activemodel (>= 3.0)
|
||||
activejob (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
activejob (4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
activemodel (4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.7.1)
|
||||
activemodel (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
activerecord (4.2.8)
|
||||
activemodel (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.7.1)
|
||||
activesupport (4.2.8)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
annotate (2.7.1)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
rake (>= 10.4, < 12.0)
|
||||
arel (6.0.3)
|
||||
arel (6.0.4)
|
||||
aws-sdk (2.5.3)
|
||||
aws-sdk-resources (= 2.5.3)
|
||||
aws-sdk-core (2.5.3)
|
||||
@ -61,21 +62,29 @@ GEM
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
builder (3.2.2)
|
||||
builder (3.2.3)
|
||||
bullet (5.4.2)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
byebug (9.0.6)
|
||||
certified (1.0.0)
|
||||
coderay (1.1.1)
|
||||
concurrent-ruby (1.0.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.2)
|
||||
debug_inspector (0.0.2)
|
||||
diff-lcs (1.2.5)
|
||||
diff-lcs (1.3)
|
||||
discourse-qunit-rails (0.0.9)
|
||||
railties
|
||||
discourse_fastimage (2.0.3)
|
||||
discourse_image_optim (0.24.4)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
image_size (~> 1.5)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
email_reply_trimmer (0.1.6)
|
||||
@ -95,7 +104,7 @@ GEM
|
||||
erubis (2.7.0)
|
||||
excon (0.53.0)
|
||||
execjs (2.7.0)
|
||||
exifr (1.2.4)
|
||||
exifr (1.2.5)
|
||||
fabrication (2.9.8)
|
||||
fakeweb (1.3.0)
|
||||
faraday (0.11.0)
|
||||
@ -105,15 +114,16 @@ GEM
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
ffi (1.9.17)
|
||||
ffi (1.9.18)
|
||||
flamegraph (0.9.5)
|
||||
foreman (0.82.0)
|
||||
thor (~> 0.19.1)
|
||||
fspath (2.1.1)
|
||||
fspath (3.1.0)
|
||||
gc_tracer (1.5.1)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
guess_html_encoding (0.0.11)
|
||||
hashdiff (0.3.2)
|
||||
hashie (3.5.5)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
@ -121,25 +131,21 @@ GEM
|
||||
http-cookie (1.0.2)
|
||||
domain_name (~> 0.5)
|
||||
http_accept_language (2.0.5)
|
||||
i18n (0.7.0)
|
||||
image_optim (0.20.2)
|
||||
exifr (~> 1.1, >= 1.1.3)
|
||||
fspath (~> 2.1)
|
||||
image_size (~> 1.3)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (1.4.1)
|
||||
in_threads (1.3.1)
|
||||
i18n (0.8.1)
|
||||
image_size (1.5.0)
|
||||
in_threads (1.4.0)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.2.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.6)
|
||||
jwt (1.5.6)
|
||||
kgio (2.10.0)
|
||||
kgio (2.11.0)
|
||||
libv8 (5.3.332.38.5)
|
||||
listen (0.7.3)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_dep (~> 1.2)
|
||||
logster (1.2.7)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
@ -151,22 +157,22 @@ GEM
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.2)
|
||||
mime-types (2.99.3)
|
||||
mini_portile2 (2.1.0)
|
||||
mini_racer (0.1.9)
|
||||
libv8 (~> 5.3)
|
||||
minitest (5.9.1)
|
||||
minitest (5.10.1)
|
||||
mocha (1.1.0)
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.15.4)
|
||||
moneta (0.8.1)
|
||||
msgpack (1.0.2)
|
||||
moneta (1.0.0)
|
||||
msgpack (1.1.0)
|
||||
multi_json (1.12.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.3)
|
||||
mustache (1.0.5)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.6.8.1)
|
||||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nokogumbo (1.4.10)
|
||||
nokogiri
|
||||
@ -206,19 +212,19 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.3)
|
||||
onebox (1.8.4)
|
||||
fast_blank (>= 1.0.0)
|
||||
htmlentities (~> 4.3.4)
|
||||
moneta (~> 0.8)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
nokogiri (~> 1.6.6)
|
||||
nokogiri (~> 1.7)
|
||||
sanitize
|
||||
openid-redis-store (0.0.2)
|
||||
redis
|
||||
ruby-openid
|
||||
pg (0.19.0)
|
||||
progress (3.1.1)
|
||||
progress (3.3.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
@ -227,6 +233,7 @@ GEM
|
||||
pry (>= 0.9.10, < 0.11.0)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
public_suffix (2.0.5)
|
||||
puma (3.6.0)
|
||||
r2 (0.2.6)
|
||||
rack (1.6.5)
|
||||
@ -239,34 +246,34 @@ GEM
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.2.7.1)
|
||||
actionmailer (= 4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activejob (= 4.2.7.1)
|
||||
activemodel (= 4.2.7.1)
|
||||
activerecord (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
rails (4.2.8)
|
||||
actionmailer (= 4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
actionview (= 4.2.8)
|
||||
activejob (= 4.2.8)
|
||||
activemodel (= 4.2.8)
|
||||
activerecord (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.7.1)
|
||||
railties (= 4.2.8)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (1.0.7)
|
||||
rails-dom-testing (1.0.8)
|
||||
activesupport (>= 4.2.0.beta, < 5.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
nokogiri (~> 1.6)
|
||||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails_multisite (1.0.6)
|
||||
rails (> 4.2, < 5)
|
||||
railties (4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
railties (4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
raindrops (0.17.0)
|
||||
rake (11.2.2)
|
||||
raindrops (0.18.0)
|
||||
rake (11.3.0)
|
||||
rake-compiler (0.9.9)
|
||||
rake
|
||||
rb-fsevent (0.9.7)
|
||||
@ -314,17 +321,17 @@ GEM
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby_dep (1.5.0)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.4.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
sass (3.2.19)
|
||||
sass-rails (5.0.4)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
sass (~> 3.1)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
sass (3.4.23)
|
||||
sassc (1.11.2)
|
||||
bundler
|
||||
ffi (~> 1.9.6)
|
||||
sass (>= 3.3.0)
|
||||
seed-fu (2.3.5)
|
||||
activerecord (>= 3.1, < 4.3)
|
||||
activesupport (>= 3.1, < 4.3)
|
||||
@ -339,8 +346,6 @@ GEM
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
sidekiq-statistic (1.2.0)
|
||||
sidekiq (>= 3.3.4, < 5)
|
||||
simple-rss (1.3.1)
|
||||
sinatra (1.4.6)
|
||||
rack (~> 1.4)
|
||||
@ -351,32 +356,36 @@ GEM
|
||||
spork-rails (4.0.0)
|
||||
rails (>= 3.0.0, < 5)
|
||||
spork (>= 1.0rc0)
|
||||
sprockets (3.6.3)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.1.1)
|
||||
sprockets-rails (3.2.0)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
stackprof (0.2.10)
|
||||
test_after_commit (1.1.0)
|
||||
activerecord (>= 3.2)
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
trollop (2.1.2)
|
||||
tzinfo (1.2.2)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.2)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.1)
|
||||
unicorn (5.2.0)
|
||||
unicorn (5.3.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.10.0)
|
||||
webmock (3.0.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@ -394,6 +403,7 @@ DEPENDENCIES
|
||||
certified
|
||||
discourse-qunit-rails
|
||||
discourse_fastimage (= 2.0.3)
|
||||
discourse_image_optim
|
||||
email_reply_trimmer (= 0.1.6)
|
||||
ember-handlebars-template (= 0.7.5)
|
||||
ember-rails (= 0.18.5)
|
||||
@ -412,8 +422,7 @@ DEPENDENCIES
|
||||
hiredis
|
||||
htmlentities
|
||||
http_accept_language (~> 2.0.5)
|
||||
image_optim (= 0.20.2)
|
||||
listen (= 0.7.3)
|
||||
listen
|
||||
logster
|
||||
lru_redux
|
||||
mail
|
||||
@ -462,12 +471,10 @@ DEPENDENCIES
|
||||
rtlit
|
||||
ruby-readability
|
||||
sanitize
|
||||
sass
|
||||
sass-rails
|
||||
sassc
|
||||
seed-fu (~> 2.3.5)
|
||||
shoulda
|
||||
sidekiq
|
||||
sidekiq-statistic
|
||||
simple-rss
|
||||
sinatra
|
||||
spork-rails
|
||||
@ -478,6 +485,7 @@ DEPENDENCIES
|
||||
uglifier
|
||||
unf
|
||||
unicorn
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.6
|
||||
|
||||
32
README.md
32
README.md
@ -1,24 +1,24 @@
|
||||
<a href="http://www.discourse.org/"></a>
|
||||
|
||||
Discourse is the 100% open source discussion platform built for the next decade of the Internet. It works as:
|
||||
Discourse is the 100% open source discussion platform built for the next decade of the Internet. Use it as a:
|
||||
|
||||
- a mailing list
|
||||
- a discussion forum
|
||||
- a long-form chat room
|
||||
- mailing list
|
||||
- discussion forum
|
||||
- long-form chat room
|
||||
|
||||
To learn more about the philosophy and goals of the project, [visit **discourse.org**](http://www.discourse.org).
|
||||
|
||||
## Screenshots
|
||||
|
||||
<a href="https://bbs.boingboing.net"><img src="https://www.discourse.org/faq/14/boing-boing-discourse.png" width="720px"></a>
|
||||
<a href="https://twittercommunity.com/"><img src="https://www.discourse.org/faq/17/twitter-discourse.png" width="720px"></a>
|
||||
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/17/how-to-geek-discourse.png" width="720px"></a>
|
||||
<a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/17/turtle-rock-discourse.png" width="720px"></a>
|
||||
|
||||
<a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/17/nexus-7-2013-mobile-discourse.png" alt="Atom" width="430px"></a>
|
||||
<a href="//discourse.soylent.com"><img src="https://www.discourse.org/faq/15/iphone-5s-mobile-discourse.png" alt="Soylent" width="270px"></a>
|
||||
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://cloud.githubusercontent.com/assets/1385470/25397876/3fe6cdac-29c0-11e7-8a41-9d0c0279f5a3.png" width="720px"></a>
|
||||
<a href="https://twittercommunity.com/"><img src="https://cloud.githubusercontent.com/assets/1385470/25397920/71b24e4c-29c0-11e7-8bcf-7a47b888412e.png" width="720px"></a>
|
||||
<a href="http://discuss.howtogeek.com"><img src="https://cloud.githubusercontent.com/assets/1385470/25398049/f0995962-29c0-11e7-99d7-a3b9c4f0b357.png" width="720px"></a>
|
||||
<a href="https://talk.turtlerockstudios.com/"><img src="https://cloud.githubusercontent.com/assets/1385470/25398115/2d560d96-29c1-11e7-9a96-b0134a4fedff.png" width="720px"></a>
|
||||
|
||||
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
|
||||
<img src="https://www.discourse.org/a/img/about/mobile-devices-2x.jpg" alt="Mobile" width="414">
|
||||
|
||||
Browse [lots more notable Discourse instances](https://www.discourse.org/customers).
|
||||
|
||||
## Development
|
||||
|
||||
@ -38,12 +38,12 @@ If you're looking for business class hosting, see [discourse.org/buy](https://ww
|
||||
|
||||
Discourse is built for the *next* 10 years of the Internet, so our requirements are high:
|
||||
|
||||
| Browsers | Tablets | Smartphones |
|
||||
| Browsers | Tablets | Phones |
|
||||
| -------- | ------- | ----------- |
|
||||
| Safari 6.1+| iPad 2+ | iOS 7+ |
|
||||
| Google Chrome 23+ | Android 4.3+ | Android 4.3+ |
|
||||
| Internet Explorer 11+ | Windows 8 | Windows Phone 8 |
|
||||
| Firefox 16+ | |
|
||||
| Safari 6.1+ | iPad 3+ | iOS 8+ |
|
||||
| Google Chrome 32+ | Android 4.3+ | Android 4.3+ |
|
||||
| Internet Explorer 11+ | | |
|
||||
| Firefox 27+ | | |
|
||||
|
||||
## Built With
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
20
app/assets/javascripts/admin/adapters/theme.js.es6
Normal file
20
app/assets/javascripts/admin/adapters/theme.js.es6
Normal file
@ -0,0 +1,20 @@
|
||||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/";
|
||||
},
|
||||
|
||||
afterFindAll(results) {
|
||||
let map = {};
|
||||
results.forEach(theme => {map[theme.id] = theme;});
|
||||
results.forEach(theme => {
|
||||
let mapped = theme.get("child_themes") || [];
|
||||
mapped = mapped.map(t => map[t.id]);
|
||||
theme.set("childThemes", mapped);
|
||||
});
|
||||
return results;
|
||||
},
|
||||
|
||||
jsonMode: true
|
||||
});
|
||||
@ -7,6 +7,13 @@ export default Ember.Component.extend({
|
||||
_editor: null,
|
||||
_skipContentChangeEvent: null,
|
||||
|
||||
@observes('editorId')
|
||||
editorIdChanged() {
|
||||
if (this.get('autofocus')) {
|
||||
this.send('focus');
|
||||
}
|
||||
},
|
||||
|
||||
@observes('content')
|
||||
contentChanged() {
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
@ -14,6 +21,13 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@observes('mode')
|
||||
modeChanged() {
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setMode("ace/mode/" + this.get('mode'));
|
||||
}
|
||||
},
|
||||
|
||||
_destroyEditor: function() {
|
||||
if (this._editor) {
|
||||
this._editor.destroy();
|
||||
@ -41,6 +55,7 @@ export default Ember.Component.extend({
|
||||
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setOptions({fontSize: "14px"});
|
||||
editor.getSession().setMode("ace/mode/" + this.get('mode'));
|
||||
editor.on('change', () => {
|
||||
this._skipContentChangeEvent = true;
|
||||
@ -48,6 +63,7 @@ export default Ember.Component.extend({
|
||||
this._skipContentChangeEvent = false;
|
||||
});
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.renderer.setScrollMargin(10,10);
|
||||
|
||||
this.$().data('editor', editor);
|
||||
this._editor = editor;
|
||||
@ -55,7 +71,20 @@ export default Ember.Component.extend({
|
||||
// xxx: don't run during qunit tests
|
||||
this.appEvents.on('ace:resize', self, self.resize);
|
||||
}
|
||||
|
||||
if (this.get("autofocus")) {
|
||||
this.send("focus");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
focus() {
|
||||
if (this._editor) {
|
||||
this._editor.focus();
|
||||
this._editor.navigateFileEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import {default as loadScript, loadCSS } from 'discourse/lib/load-script';
|
||||
|
||||
/**
|
||||
An input field for a color.
|
||||
|
||||
@ -6,19 +8,36 @@
|
||||
@params valid is a boolean indicating if the input field is a valid color.
|
||||
**/
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['color-picker'],
|
||||
hexValueChanged: function() {
|
||||
var hex = this.get('hexValue');
|
||||
let $text = this.$('input.hex-input');
|
||||
|
||||
if (this.get('valid')) {
|
||||
this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||
$text.attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||
|
||||
if (this.get('pickerLoaded')) {
|
||||
this.$('.picker').spectrum({color: "#" + this.get('hexValue')});
|
||||
}
|
||||
} else {
|
||||
this.$('input').attr('style', '');
|
||||
$text.attr('style', '');
|
||||
}
|
||||
}.observes('hexValue', 'brightnessValue', 'valid'),
|
||||
|
||||
_triggerHexChanged: function() {
|
||||
var self = this;
|
||||
Em.run.schedule('afterRender', function() {
|
||||
self.hexValueChanged();
|
||||
didInsertElement() {
|
||||
loadScript('/javascripts/spectrum.js').then(()=>{
|
||||
loadCSS('/javascripts/spectrum.css').then(()=>{
|
||||
Em.run.schedule('afterRender', ()=>{
|
||||
this.$('.picker').spectrum({color: "#" + this.get('hexValue')})
|
||||
.on("change.spectrum", (me, color)=>{
|
||||
this.set('hexValue', color.toHexString().replace("#",""));
|
||||
});
|
||||
this.set('pickerLoaded', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}.on('didInsertElement')
|
||||
Em.run.schedule('afterRender', ()=>{
|
||||
this.hexValueChanged();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
router: function() {
|
||||
return getOwner(this).lookup('router:main');
|
||||
}.property(),
|
||||
|
||||
active: function() {
|
||||
const id = this.get('customization.id');
|
||||
return this.get('router.url').indexOf(`/customize/css_html/${id}/css`) !== -1;
|
||||
}.property('router.url', 'customization.id')
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
import {default as computed, observes} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
init(){
|
||||
this._super();
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
classNames: ['inline-edit'],
|
||||
|
||||
@observes("checked")
|
||||
checkedChanged() {
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
@computed("labelKey")
|
||||
label(key) {
|
||||
return I18n.t(key);
|
||||
},
|
||||
|
||||
@computed("checked", "checkedInternal")
|
||||
changed(checked, checkedInternal) {
|
||||
return (!!checked) !== (!!checkedInternal);
|
||||
},
|
||||
|
||||
actions: {
|
||||
cancelled(){
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
finished(){
|
||||
this.set("checked", this.get("checkedInternal"));
|
||||
this.sendAction();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,74 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed("model.colors","onlyOverridden")
|
||||
colors(allColors, onlyOverridden) {
|
||||
if (onlyOverridden) {
|
||||
return allColors.filter(color => color.get("overridden"));
|
||||
} else {
|
||||
return allColors;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
revert: function(color) {
|
||||
color.revert();
|
||||
},
|
||||
|
||||
undo: function(color) {
|
||||
color.undo();
|
||||
},
|
||||
|
||||
copyToClipboard() {
|
||||
$(".table.colors").hide();
|
||||
let area = $("<textarea id='copy-range'></textarea>");
|
||||
$(".table.colors").after(area);
|
||||
area.text(this.get("model").schemeJson());
|
||||
let range = document.createRange();
|
||||
range.selectNode(area[0]);
|
||||
window.getSelection().addRange(range);
|
||||
let successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.set("model.savingStatus", I18n.t("admin.customize.copied_to_clipboard"));
|
||||
} else {
|
||||
this.set("model.savingStatus", I18n.t("admin.customize.copy_to_clipboard_error"));
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
this.set("model.savingStatus", null);
|
||||
}, 2000);
|
||||
|
||||
window.getSelection().removeAllRanges();
|
||||
|
||||
$(".table.colors").show();
|
||||
$(area).remove();
|
||||
},
|
||||
|
||||
copy() {
|
||||
var newColorScheme = Em.copy(this.get('model'), true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + this.get('model.name'));
|
||||
newColorScheme.save().then(()=>{
|
||||
this.get('allColors').pushObject(newColorScheme);
|
||||
this.replaceRoute('adminCustomize.colors.show', newColorScheme);
|
||||
});
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.get('model').save();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
|
||||
const model = this.get('model');
|
||||
return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
model.destroy().then(()=>{
|
||||
this.get('allColors').removeObject(model);
|
||||
this.replaceRoute('adminCustomize.colors');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,10 +1,14 @@
|
||||
export default Ember.Controller.extend({
|
||||
onlyOverridden: false,
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
baseColorScheme: function() {
|
||||
return this.get('model').findBy('is_base', true);
|
||||
}.property('model.@each.id'),
|
||||
|
||||
baseColorSchemes: function() {
|
||||
return this.get('model').filterBy('is_base', true);
|
||||
}.property('model.@each.id'),
|
||||
|
||||
baseColors: function() {
|
||||
var baseColorsHash = Em.Object.create({});
|
||||
_.each(this.get('baseColorScheme.colors'), function(color){
|
||||
@ -13,99 +17,25 @@ export default Ember.Controller.extend({
|
||||
return baseColorsHash;
|
||||
}.property('baseColorScheme'),
|
||||
|
||||
removeSelected() {
|
||||
this.get('model').removeObject(this.get('selectedItem'));
|
||||
this.set('selectedItem', null);
|
||||
},
|
||||
|
||||
filterContent: function() {
|
||||
if (!this.get('selectedItem')) { return; }
|
||||
|
||||
if (!this.get('onlyOverridden')) {
|
||||
this.set('colors', this.get('selectedItem.colors'));
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
_.each(this.get('selectedItem.colors'), function(color){
|
||||
if (color.get('overridden')) matches.pushObject(color);
|
||||
});
|
||||
|
||||
this.set('colors', matches);
|
||||
}.observes('onlyOverridden'),
|
||||
|
||||
updateEnabled: function() {
|
||||
var selectedItem = this.get('selectedItem');
|
||||
if (selectedItem.get('enabled')) {
|
||||
this.get('model').forEach(function(c) {
|
||||
if (c !== selectedItem) {
|
||||
c.set('enabled', false);
|
||||
c.startTrackingChanges();
|
||||
c.notifyPropertyChange('description');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectColorScheme: function(colorScheme) {
|
||||
if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); }
|
||||
this.set('selectedItem', colorScheme);
|
||||
this.set('colors', colorScheme.get('colors'));
|
||||
colorScheme.set('savingStatus', null);
|
||||
colorScheme.set('selected', true);
|
||||
this.filterContent();
|
||||
|
||||
newColorSchemeWithBase(baseKey) {
|
||||
const base = this.get('baseColorSchemes').findBy('base_scheme_id', baseKey);
|
||||
const newColorScheme = Em.copy(base, true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||
newColorScheme.set('base_scheme_id', base.get('base_scheme_id'));
|
||||
newColorScheme.save().then(()=>{
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
newColorScheme.set('savingStatus', null);
|
||||
this.replaceRoute('adminCustomize.colors.show', newColorScheme);
|
||||
});
|
||||
},
|
||||
|
||||
newColorScheme() {
|
||||
const newColorScheme = Em.copy(this.get('baseColorScheme'), true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
this.send('selectColorScheme', newColorScheme);
|
||||
this.set('onlyOverridden', false);
|
||||
showModal('admin-color-scheme-select-base', { model: this.get('baseColorSchemes'), admin: true});
|
||||
},
|
||||
|
||||
revert: function(color) {
|
||||
color.revert();
|
||||
},
|
||||
|
||||
undo: function(color) {
|
||||
color.undo();
|
||||
},
|
||||
|
||||
toggleEnabled: function() {
|
||||
var selectedItem = this.get('selectedItem');
|
||||
selectedItem.toggleProperty('enabled');
|
||||
selectedItem.save({enabledOnly: true});
|
||||
this.updateEnabled();
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.get('selectedItem').save();
|
||||
this.updateEnabled();
|
||||
},
|
||||
|
||||
copy(colorScheme) {
|
||||
var newColorScheme = Em.copy(colorScheme, true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + colorScheme.get('name'));
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
this.send('selectColorScheme', newColorScheme);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
var self = this,
|
||||
item = self.get('selectedItem');
|
||||
|
||||
return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
if (item.get('newRecord')) {
|
||||
self.removeSelected();
|
||||
} else {
|
||||
item.destroy().then(function(){ self.removeSelected(); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import { url } from 'discourse/lib/computed';
|
||||
|
||||
const sections = ['css', 'header', 'top', 'footer', 'head-tag', 'body-tag',
|
||||
'mobile-css', 'mobile-header', 'mobile-top', 'mobile-footer',
|
||||
'embedded-css'];
|
||||
|
||||
const activeSections = {};
|
||||
sections.forEach(function(s) {
|
||||
activeSections[Ember.String.camelize(s) + "Active"] = Ember.computed.equal('section', s);
|
||||
});
|
||||
|
||||
|
||||
export default Ember.Controller.extend(activeSections, {
|
||||
maximized: false,
|
||||
section: null,
|
||||
|
||||
previewUrl: url("model.key", "/?preview-style=%@"),
|
||||
downloadUrl: url('model.id', '/admin/site_customizations/%@'),
|
||||
|
||||
mobile: function() {
|
||||
return this.get('section').indexOf('mobile-') === 0;
|
||||
}.property('section'),
|
||||
|
||||
maximizeIcon: function() {
|
||||
return this.get('maximized') ? 'compress' : 'expand';
|
||||
}.property('maximized'),
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save');
|
||||
}.property('model.isSaving'),
|
||||
|
||||
saveDisabled: function() {
|
||||
return !this.get('model.changed') || this.get('model.isSaving');
|
||||
}.property('model.changed', 'model.isSaving'),
|
||||
|
||||
adminCustomizeCssHtml: Ember.inject.controller(),
|
||||
|
||||
undoPreviewUrl: url('/?preview-style='),
|
||||
defaultStyleUrl: url('/?preview-style=default'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('model').saveChanges();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
const model = this.get('model');
|
||||
model.destroyRecord().then(() => {
|
||||
this.get('adminCustomizeCssHtml').get('model').removeObject(model);
|
||||
this.transitionToRoute('adminCustomizeCssHtml');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleMaximize: function() {
|
||||
this.toggleProperty('maximized');
|
||||
},
|
||||
|
||||
toggleMobile: function() {
|
||||
const section = this.get('section');
|
||||
|
||||
// Try to send to the same tab as before
|
||||
let dest;
|
||||
if (this.get('mobile')) {
|
||||
dest = section.replace('mobile-', '');
|
||||
if (sections.indexOf(dest) === -1) { dest = 'css'; }
|
||||
} else {
|
||||
dest = 'mobile-' + section;
|
||||
if (sections.indexOf(dest) === -1) { dest = 'mobile-css'; }
|
||||
}
|
||||
this.replaceRoute('adminCustomizeCssHtml.show', this.get('model.id'), dest);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,156 @@
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
maximized: false,
|
||||
section: null,
|
||||
|
||||
targets: [
|
||||
{id: 0, name: I18n.t('admin.customize.theme.common')},
|
||||
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
|
||||
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
|
||||
],
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showCommon() {
|
||||
return this.shouldShow('common');
|
||||
},
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showDesktop() {
|
||||
return this.shouldShow('desktop');
|
||||
},
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showMobile() {
|
||||
return this.shouldShow('mobile');
|
||||
},
|
||||
|
||||
@observes('onlyOverridden')
|
||||
onlyOverriddenChanged() {
|
||||
if (this.get('onlyOverridden')) {
|
||||
if (!this.get('model').hasEdited(this.get('currentTargetName'), this.get('fieldName'))) {
|
||||
let target = (this.get('showCommon') && 'common') ||
|
||||
(this.get('showDesktop') && 'desktop') ||
|
||||
(this.get('showMobile') && 'mobile');
|
||||
|
||||
let fields = this.get('model.theme_fields');
|
||||
let field = fields && fields.find(f => (f.target === target));
|
||||
this.replaceRoute('adminCustomizeThemes.edit', this.get('model.id'), target, field && field.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
shouldShow(target){
|
||||
if(!this.get("onlyOverridden")) {
|
||||
return true;
|
||||
}
|
||||
return this.get("model").hasEdited(target);
|
||||
},
|
||||
|
||||
currentTarget: 0,
|
||||
|
||||
setTargetName: function(name) {
|
||||
let target;
|
||||
switch(name) {
|
||||
case "common": target = 0; break;
|
||||
case "desktop": target = 1; break;
|
||||
case "mobile": target = 2; break;
|
||||
}
|
||||
|
||||
this.set("currentTarget", target);
|
||||
},
|
||||
|
||||
@computed("currentTarget")
|
||||
currentTargetName(target) {
|
||||
switch(parseInt(target)) {
|
||||
case 0: return "common";
|
||||
case 1: return "desktop";
|
||||
case 2: return "mobile";
|
||||
}
|
||||
},
|
||||
|
||||
@computed("fieldName")
|
||||
activeSectionMode(fieldName) {
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||
},
|
||||
|
||||
@computed("currentTargetName", "fieldName", "saving")
|
||||
error(target, fieldName) {
|
||||
return this.get('model').getError(target, fieldName);
|
||||
},
|
||||
|
||||
@computed("fieldName", "currentTargetName")
|
||||
editorId(fieldName, currentTarget) {
|
||||
return fieldName + "|" + currentTarget;
|
||||
},
|
||||
|
||||
@computed("fieldName", "currentTargetName", "model")
|
||||
activeSection: {
|
||||
get(fieldName, target, model) {
|
||||
return model.getField(target, fieldName);
|
||||
},
|
||||
set(value, fieldName, target, model) {
|
||||
model.setField(target, fieldName, value);
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("currentTarget", "onlyOverridden")
|
||||
fields(target, onlyOverridden) {
|
||||
let fields = [
|
||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
||||
];
|
||||
|
||||
if (parseInt(target) === 0) {
|
||||
fields.push("embedded_scss");
|
||||
}
|
||||
|
||||
if (onlyOverridden) {
|
||||
const model = this.get("model");
|
||||
const targetName = this.get("currentTargetName");
|
||||
fields = fields.filter(name => model.hasEdited(targetName, name));
|
||||
}
|
||||
|
||||
return fields.map(name=>{
|
||||
let hash = {
|
||||
key: (`admin.customize.theme.${name}.text`),
|
||||
name: name
|
||||
};
|
||||
|
||||
if (name.indexOf("_tag") > 0) {
|
||||
hash.icon = "file-text-o";
|
||||
}
|
||||
|
||||
hash.title = I18n.t(`admin.customize.theme.${name}.title`);
|
||||
|
||||
return hash;
|
||||
});
|
||||
},
|
||||
|
||||
previewUrl: url('model.id', '/admin/themes/%@/preview'),
|
||||
|
||||
maximizeIcon: function() {
|
||||
return this.get('maximized') ? 'compress' : 'expand';
|
||||
}.property('maximized'),
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save');
|
||||
}.property('model.isSaving'),
|
||||
|
||||
saveDisabled: function() {
|
||||
return !this.get('model.changed') || this.get('model.isSaving');
|
||||
}.property('model.changed', 'model.isSaving'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saving', true);
|
||||
this.get('model').saveChanges("theme_fields").finally(()=>{this.set('saving', false);});
|
||||
},
|
||||
|
||||
toggleMaximize: function() {
|
||||
this.toggleProperty('maximized');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,175 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
@computed("model", "allThemes")
|
||||
parentThemes(model, allThemes) {
|
||||
let parents = allThemes.filter(theme =>
|
||||
_.contains(theme.get("childThemes"), model));
|
||||
return parents.length === 0 ? null : parents;
|
||||
},
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
hasEditedFields(fields) {
|
||||
return fields.any(f=>!Em.isBlank(f.value));
|
||||
},
|
||||
|
||||
@computed('model.theme_fields.@each')
|
||||
editedDescriptions(fields) {
|
||||
let descriptions = [];
|
||||
let description = target => {
|
||||
let current = fields.filter(field => field.target === target && !Em.isBlank(field.value));
|
||||
if (current.length > 0) {
|
||||
let text = I18n.t('admin.customize.theme.'+target);
|
||||
let localized = current.map(f=>I18n.t('admin.customize.theme.'+f.name + '.text'));
|
||||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
['common','desktop','mobile'].forEach(target=> {
|
||||
descriptions.push(description(target));
|
||||
});
|
||||
return descriptions.reject(d=>Em.isBlank(d));
|
||||
},
|
||||
|
||||
previewUrl: url('model.id', '/admin/themes/%@/preview'),
|
||||
|
||||
@computed("colorSchemeId", "model.color_scheme_id")
|
||||
colorSchemeChanged(colorSchemeId, existingId) {
|
||||
colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId);
|
||||
return colorSchemeId !== existingId;
|
||||
},
|
||||
|
||||
@computed("availableChildThemes", "model.childThemes.@each", "model", "allowChildThemes")
|
||||
selectableChildThemes(available, childThemes, model, allowChildThemes) {
|
||||
if (!allowChildThemes && (!childThemes || childThemes.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let themes = [];
|
||||
available.forEach(t=> {
|
||||
if (!childThemes || (childThemes.indexOf(t) === -1)) {
|
||||
themes.push(t);
|
||||
};
|
||||
});
|
||||
return themes.length === 0 ? null : themes;
|
||||
},
|
||||
|
||||
@computed("allThemes", "allThemes.length", "model")
|
||||
availableChildThemes(allThemes, count) {
|
||||
if (count === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let excludeIds = [this.get("model.id")];
|
||||
|
||||
let themes = [];
|
||||
allThemes.forEach(theme => {
|
||||
if (excludeIds.indexOf(theme.get("id")) === -1) {
|
||||
themes.push(theme);
|
||||
}
|
||||
});
|
||||
|
||||
return themes;
|
||||
},
|
||||
|
||||
downloadUrl: url('model.id', '/admin/themes/%@'),
|
||||
|
||||
actions: {
|
||||
|
||||
updateToLatest() {
|
||||
this.set("updatingRemote", true);
|
||||
this.get("model").updateToLatest()
|
||||
.catch(popupAjaxError)
|
||||
.finally(()=>{
|
||||
this.set("updatingRemote", false);
|
||||
});
|
||||
},
|
||||
|
||||
checkForThemeUpdates() {
|
||||
this.set("updatingRemote", true);
|
||||
this.get("model").checkForUpdates()
|
||||
.catch(popupAjaxError)
|
||||
.finally(()=>{
|
||||
this.set("updatingRemote", false);
|
||||
});
|
||||
},
|
||||
|
||||
cancelChangeScheme() {
|
||||
this.set("colorSchemeId", this.get("model.color_scheme_id"));
|
||||
},
|
||||
changeScheme(){
|
||||
let schemeId = this.get("colorSchemeId");
|
||||
this.set("model.color_scheme_id", schemeId === null ? null : parseInt(schemeId));
|
||||
this.get("model").saveChanges("color_scheme_id");
|
||||
},
|
||||
startEditingName() {
|
||||
this.set("oldName", this.get("model.name"));
|
||||
this.set("editingName", true);
|
||||
},
|
||||
cancelEditingName() {
|
||||
this.set("model.name", this.get("oldName"));
|
||||
this.set("editingName", false);
|
||||
},
|
||||
finishedEditingName() {
|
||||
this.get("model").saveChanges("name");
|
||||
this.set("editingName", false);
|
||||
},
|
||||
|
||||
editTheme() {
|
||||
let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss');
|
||||
|
||||
if (this.get("model.remote_theme")) {
|
||||
bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => {
|
||||
if (result) {
|
||||
edit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
edit();
|
||||
}
|
||||
},
|
||||
|
||||
applyDefault() {
|
||||
const model = this.get("model");
|
||||
model.saveChanges("default").then(()=>{
|
||||
if (model.get("default")) {
|
||||
this.get("allThemes").forEach(theme=>{
|
||||
if (theme !== model && theme.get('default')) {
|
||||
theme.set("default", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
applyUserSelectable() {
|
||||
this.get("model").saveChanges("user_selectable");
|
||||
},
|
||||
|
||||
addChildTheme() {
|
||||
let themeId = parseInt(this.get("selectedChildThemeId"));
|
||||
let theme = this.get("allThemes").findBy("id", themeId);
|
||||
this.get("model").addChildTheme(theme);
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
this.get("model").removeChildTheme(theme);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
const model = this.get('model');
|
||||
model.destroyRecord().then(() => {
|
||||
this.get('allThemes').removeObject(model);
|
||||
this.transitionToRoute('adminCustomizeThemes');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed('model', 'model.@each')
|
||||
sortedThemes(themes) {
|
||||
return _.sortBy(themes.content, t => {
|
||||
return [!t.get("default"), !t.get("user_selectable"), t.get("name").toLowerCase()];
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -22,9 +22,9 @@ export default Ember.Controller.extend({
|
||||
];
|
||||
}.property(),
|
||||
|
||||
@computed('model.visible', 'model.public', 'model.alias_level')
|
||||
@computed('model.visible', 'model.public')
|
||||
disableMembershipRequestSetting(visible, publicGroup) {
|
||||
return !visible || publicGroup || !this.get('model.canEveryoneMention');
|
||||
return !visible || publicGroup;
|
||||
},
|
||||
|
||||
@computed('model.visible', 'model.allow_membership_requests')
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsBulk: Ember.inject.controller(),
|
||||
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
|
||||
});
|
||||
@ -6,6 +6,7 @@ export default Ember.Controller.extend({
|
||||
users: null,
|
||||
groupId: null,
|
||||
saving: false,
|
||||
bulkAddResponse: null,
|
||||
|
||||
@computed('saving', 'users', 'groupId')
|
||||
buttonDisabled(saving, users, groupId) {
|
||||
@ -24,7 +25,8 @@ export default Ember.Controller.extend({
|
||||
ajax('/admin/groups/bulk', {
|
||||
data: { users, group_id: this.get('groupId') },
|
||||
method: 'PUT'
|
||||
}).then(() => {
|
||||
}).then(result => {
|
||||
this.set('bulkAddResponse', result);
|
||||
this.transitionToRoute('adminGroups.bulkComplete');
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('saving', false);
|
||||
|
||||
@ -4,7 +4,7 @@ import Report from 'admin/models/report';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["mode", "start-date", "end-date", "category-id", "group-id"],
|
||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
||||
viewMode: 'graph',
|
||||
viewingTable: Em.computed.equal('viewMode', 'table'),
|
||||
viewingGraph: Em.computed.equal('viewMode', 'graph'),
|
||||
@ -28,7 +28,15 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed('model.type')
|
||||
showCategoryOptions(modelType) {
|
||||
return !modelType.match(/_private_messages$/) && !modelType.match(/^page_view_/);
|
||||
return [
|
||||
'topics',
|
||||
'posts',
|
||||
'time_to_first_response_total',
|
||||
'topics_with_no_response',
|
||||
'flags',
|
||||
'likes',
|
||||
'bookmarks'
|
||||
].includes(modelType);
|
||||
},
|
||||
|
||||
@computed('model.type')
|
||||
@ -42,13 +50,13 @@ export default Ember.Controller.extend({
|
||||
this.set("refreshing", true);
|
||||
|
||||
this.setProperties({
|
||||
'start-date': this.get('startDate'),
|
||||
'end-date': this.get('endDate'),
|
||||
'category-id': this.get('categoryId'),
|
||||
'start_date': this.get('startDate'),
|
||||
'end_date': this.get('endDate'),
|
||||
'category_id': this.get('categoryId'),
|
||||
});
|
||||
|
||||
if (this.get('groupId')){
|
||||
this.set('group-id', this.get('groupId'));
|
||||
this.set('group_id', this.get('groupId'));
|
||||
}
|
||||
|
||||
q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"), this.get("groupId"));
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
adminCustomizeColors: Ember.inject.controller(),
|
||||
|
||||
actions: {
|
||||
selectBase() {
|
||||
this.get('adminCustomizeColors')
|
||||
.send('newColorSchemeWithBase', this.get('selectedBaseThemeId'));
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
// import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
local: Ember.computed.equal('selection', 'local'),
|
||||
remote: Ember.computed.equal('selection', 'remote'),
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
|
||||
actions: {
|
||||
importTheme() {
|
||||
|
||||
let options = {
|
||||
type: 'POST'
|
||||
};
|
||||
|
||||
if (this.get('local')) {
|
||||
options.processData = false;
|
||||
options.contentType = false;
|
||||
options.data = new FormData();
|
||||
options.data.append('theme', $('#file-input')[0].files[0]);
|
||||
} else {
|
||||
options.data = {remote: this.get('uploadUrl')};
|
||||
}
|
||||
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loadDiff() {
|
||||
this.set('loading', true);
|
||||
ajax('/admin/logs/staff_action_logs/' + this.get('model.id') + '/diff')
|
||||
.then(diff=>{
|
||||
this.set('loading', false);
|
||||
this.set('diff', diff.side_by_side);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1,20 +0,0 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
previousSelected: Ember.computed.equal('selectedTab', 'previous'),
|
||||
newSelected: Ember.computed.equal('selectedTab', 'new'),
|
||||
|
||||
onShow: function() {
|
||||
this.send("selectNew");
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectNew: function() {
|
||||
this.set('selectedTab', 'new');
|
||||
},
|
||||
|
||||
selectPrevious: function() {
|
||||
this.set('selectedTab', 'previous');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details";
|
||||
|
||||
export default ChangeSiteCustomizationDetailsController.extend({
|
||||
onShow() {
|
||||
this.send("selectPrevious");
|
||||
}
|
||||
});
|
||||
@ -9,18 +9,26 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||
},
|
||||
|
||||
description: function() {
|
||||
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||
return "" + this.name;
|
||||
}.property(),
|
||||
|
||||
startTrackingChanges: function() {
|
||||
this.set('originals', {
|
||||
name: this.get('name'),
|
||||
enabled: this.get('enabled')
|
||||
name: this.get('name')
|
||||
});
|
||||
},
|
||||
|
||||
schemeJson(){
|
||||
let buffer = [];
|
||||
_.each(this.get('colors'), (c) => {
|
||||
buffer.push(` "${c.get('name')}": "${c.get('hex')}"`);
|
||||
});
|
||||
|
||||
return [`"${this.get("name")}": {`, buffer.join(",\n"), "}"].join("\n");
|
||||
},
|
||||
|
||||
copy: function() {
|
||||
var newScheme = ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()});
|
||||
var newScheme = ColorScheme.create({name: this.get('name'), can_edit: true, colors: Em.A()});
|
||||
_.each(this.get('colors'), function(c){
|
||||
newScheme.colors.pushObject(ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex'), default_hex: c.get('default_hex')}));
|
||||
});
|
||||
@ -29,19 +37,16 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||
|
||||
changed: function() {
|
||||
if (!this.originals) return false;
|
||||
if (this.originals['name'] !== this.get('name') || this.originals['enabled'] !== this.get('enabled')) return true;
|
||||
if (this.originals['name'] !== this.get('name')) return true;
|
||||
if (_.any(this.get('colors'), function(c){ return c.get('changed'); })) return true;
|
||||
return false;
|
||||
}.property('name', 'enabled', 'colors.@each.changed', 'saving'),
|
||||
}.property('name', 'colors.@each.changed', 'saving'),
|
||||
|
||||
disableSave: function() {
|
||||
if (this.get('theme_id')) { return false; }
|
||||
return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); });
|
||||
}.property('changed'),
|
||||
|
||||
disableEnable: function() {
|
||||
return !this.get('id') || this.get('saving');
|
||||
}.property('id', 'saving'),
|
||||
|
||||
newRecord: function() {
|
||||
return (!this.get('id'));
|
||||
}.property('id'),
|
||||
@ -53,11 +58,11 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||
this.set('savingStatus', I18n.t('saving'));
|
||||
this.set('saving',true);
|
||||
|
||||
var data = { enabled: this.enabled };
|
||||
var data = {};
|
||||
|
||||
if (!opts || !opts.enabledOnly) {
|
||||
data.name = this.name;
|
||||
|
||||
data.base_scheme_id = this.get('base_scheme_id');
|
||||
data.colors = [];
|
||||
_.each(this.get('colors'), function(c) {
|
||||
if (!self.id || c.get('changed')) {
|
||||
@ -78,8 +83,6 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||
_.each(self.get('colors'), function(c) {
|
||||
c.startTrackingChanges();
|
||||
});
|
||||
} else {
|
||||
self.set('originals.enabled', data.enabled);
|
||||
}
|
||||
self.set('savingStatus', I18n.t('saved'));
|
||||
self.set('saving', false);
|
||||
@ -96,30 +99,25 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||
});
|
||||
|
||||
var ColorSchemes = Ember.ArrayProxy.extend({
|
||||
selectedItemChanged: function() {
|
||||
var selected = this.get('selectedItem');
|
||||
_.each(this.get('content'),function(i) {
|
||||
return i.set('selected', selected === i);
|
||||
});
|
||||
}.observes('selectedItem')
|
||||
});
|
||||
|
||||
ColorScheme.reopenClass({
|
||||
findAll: function() {
|
||||
var colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
||||
ajax('/admin/color_schemes').then(function(all) {
|
||||
return ajax('/admin/color_schemes').then(function(all) {
|
||||
_.each(all, function(colorScheme){
|
||||
colorSchemes.pushObject(ColorScheme.create({
|
||||
id: colorScheme.id,
|
||||
name: colorScheme.name,
|
||||
enabled: colorScheme.enabled,
|
||||
is_base: colorScheme.is_base,
|
||||
theme_id: colorScheme.theme_id,
|
||||
theme_name: colorScheme.theme_name,
|
||||
base_scheme_id: colorScheme.base_scheme_id,
|
||||
colors: colorScheme.colors.map(function(c) { return ColorSchemeColor.create({name: c.name, hex: c.hex, default_hex: c.default_hex}); })
|
||||
}));
|
||||
});
|
||||
colorSchemes.set('loading', false);
|
||||
return colorSchemes;
|
||||
});
|
||||
return colorSchemes;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import RestModel from 'discourse/models/rest';
|
||||
|
||||
const trackedProperties = [
|
||||
'enabled', 'name', 'stylesheet', 'header', 'top', 'footer', 'mobile_stylesheet',
|
||||
'mobile_header', 'mobile_top', 'mobile_footer', 'head_tag', 'body_tag', 'embedded_css'
|
||||
];
|
||||
|
||||
function changed() {
|
||||
const originals = this.get('originals');
|
||||
if (!originals) { return false; }
|
||||
return _.some(trackedProperties, (p) => originals[p] !== this.get(p));
|
||||
}
|
||||
|
||||
const SiteCustomization = RestModel.extend({
|
||||
description: function() {
|
||||
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||
}.property('selected', 'name', 'enabled'),
|
||||
|
||||
changed: changed.property.apply(changed, trackedProperties.concat('originals')),
|
||||
|
||||
startTrackingChanges: function() {
|
||||
this.set('originals', this.getProperties(trackedProperties));
|
||||
}.on('init'),
|
||||
|
||||
saveChanges() {
|
||||
return this.save(this.getProperties(trackedProperties)).then(() => this.startTrackingChanges());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default SiteCustomization;
|
||||
@ -39,7 +39,7 @@ const StaffActionLog = Discourse.Model.extend({
|
||||
}.property('action_name'),
|
||||
|
||||
useCustomModalForDetails: function() {
|
||||
return _.contains(['change_site_customization', 'delete_site_customization'], this.get('action_name'));
|
||||
return _.contains(['change_theme', 'delete_theme'], this.get('action_name'));
|
||||
}.property('action_name')
|
||||
});
|
||||
|
||||
|
||||
114
app/assets/javascripts/admin/models/theme.js.es6
Normal file
114
app/assets/javascripts/admin/models/theme.js.es6
Normal file
@ -0,0 +1,114 @@
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
@computed('theme_fields')
|
||||
themeFields(fields) {
|
||||
|
||||
if (!fields) {
|
||||
this.set('theme_fields', []);
|
||||
return {};
|
||||
}
|
||||
|
||||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
hash[field.target + " " + field.name] = field;
|
||||
});
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
hasEdited(target, name){
|
||||
if (name) {
|
||||
return !Em.isEmpty(this.getField(target, name));
|
||||
} else {
|
||||
let fields = this.get("theme_fields") || [];
|
||||
return fields.any(field => (field.target === target && !Em.isEmpty(field.value)));
|
||||
}
|
||||
},
|
||||
|
||||
getError(target, name) {
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
return field ? field.error : "";
|
||||
},
|
||||
|
||||
getField(target, name) {
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
return field ? field.value : "";
|
||||
},
|
||||
|
||||
setField(target, name, value) {
|
||||
this.set("changed", true);
|
||||
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
if (!field) {
|
||||
field = {name, target, value};
|
||||
this.theme_fields.push(field);
|
||||
themeFields[key] = field;
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("childThemes.@each")
|
||||
child_theme_ids(childThemes) {
|
||||
if (childThemes) {
|
||||
return childThemes.map(theme => Ember.get(theme, "id"));
|
||||
}
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
const childThemes = this.get("childThemes");
|
||||
childThemes.removeObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
addChildTheme(theme){
|
||||
let childThemes = this.get("childThemes");
|
||||
if (!childThemes) {
|
||||
childThemes = [];
|
||||
this.set('childThemes', childThemes);
|
||||
}
|
||||
childThemes.removeObject(theme);
|
||||
childThemes.pushObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
@computed('name', 'default')
|
||||
description: function(name, isDefault) {
|
||||
if (isDefault) {
|
||||
return I18n.t('admin.customize.theme.default_name', {name: name});
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
},
|
||||
|
||||
checkForUpdates() {
|
||||
return this.save({remote_check: true})
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
updateToLatest() {
|
||||
return this.save({remote_update: true})
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
changed: false,
|
||||
|
||||
saveChanges() {
|
||||
const hash = this.getProperties.apply(this, arguments);
|
||||
return this.save(hash)
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
@ -0,0 +1,18 @@
|
||||
export default Ember.Route.extend({
|
||||
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomize.colors');
|
||||
const model = all.findBy('id', parseInt(params.scheme_id));
|
||||
return model ? model : this.replaceWith('adminCustomize.colors.index');
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
return {scheme_id: model.get('id')};
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set('model', model);
|
||||
controller.set('allColors', this.modelFor('adminCustomize.colors'));
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,9 +6,7 @@ export default Ember.Route.extend({
|
||||
return ColorScheme.findAll();
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super();
|
||||
this.controllerFor('adminCustomizeColors').set('selectedItem', null);
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeCssHtml');
|
||||
const model = all.findBy('id', parseInt(params.site_customization_id));
|
||||
return model ? { model, section: params.section } : this.replaceWith('adminCustomizeCssHtml.index');
|
||||
},
|
||||
|
||||
setupController(controller, hash) {
|
||||
controller.setProperties(hash);
|
||||
}
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('site-customization');
|
||||
},
|
||||
|
||||
actions: {
|
||||
importModal() {
|
||||
showModal('upload-customization');
|
||||
},
|
||||
|
||||
newCustomization(obj) {
|
||||
obj = obj || {name: I18n.t("admin.customize.new_style")};
|
||||
const item = this.store.createRecord('site-customization');
|
||||
|
||||
const all = this.modelFor('adminCustomizeCssHtml');
|
||||
const self = this;
|
||||
item.save(obj).then(function() {
|
||||
all.pushObject(item);
|
||||
self.transitionTo('adminCustomizeCssHtml.show', item.get('id'), 'css');
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
export default Ember.Route.extend({
|
||||
beforeModel() {
|
||||
this.transitionTo('adminCustomize.colors');
|
||||
this.transitionTo('adminCustomizeThemes');
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
const model = all.findBy('id', parseInt(params.theme_id));
|
||||
return model ? { model,
|
||||
target: params.target,
|
||||
field_name: params.field_name
|
||||
} : this.replaceWith('adminCustomizeThemes.index');
|
||||
},
|
||||
|
||||
serialize(wrapper) {
|
||||
return {
|
||||
model: wrapper.model,
|
||||
target: wrapper.target || "common",
|
||||
field_name: wrapper.field_name || "scss",
|
||||
theme_id: wrapper.model.get("id")
|
||||
};
|
||||
},
|
||||
|
||||
setupController(controller, wrapper) {
|
||||
controller.set("model", wrapper.model);
|
||||
controller.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
this.controllerFor("adminCustomizeThemes").set("editingTheme", true);
|
||||
},
|
||||
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
export default Ember.Route.extend({
|
||||
setupController() {
|
||||
this.controllerFor("adminCustomizeThemes").set("editingTheme", false);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
export default Ember.Route.extend({
|
||||
|
||||
serialize(model) {
|
||||
return {theme_id: model.get('id')};
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
const model = all.findBy('id', parseInt(params.theme_id));
|
||||
return model ? model : this.replaceWith('adminCustomizeTheme.index');
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
const parentController = this.controllerFor("adminCustomizeThemes");
|
||||
parentController.set("editingTheme", false);
|
||||
controller.set("allThemes", parentController.get("model"));
|
||||
controller.set("colorSchemes", parentController.get("model.extras.color_schemes"));
|
||||
controller.set("colorSchemeId", model.get("color_scheme_id"));
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('theme');
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super(controller, model);
|
||||
controller.set("editingTheme", false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
importModal() {
|
||||
showModal('admin-import-theme', {admin: true});
|
||||
},
|
||||
|
||||
addTheme(theme) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
all.pushObject(theme);
|
||||
this.transitionTo('adminCustomizeThemes.show', theme.get('id'));
|
||||
},
|
||||
|
||||
|
||||
newTheme(obj) {
|
||||
obj = obj || {name: I18n.t("admin.customize.new_style")};
|
||||
const item = this.store.createRecord('theme');
|
||||
|
||||
item.save(obj).then(() => {
|
||||
this.send('addTheme', item);
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -13,14 +13,9 @@ export default Discourse.Route.extend({
|
||||
},
|
||||
|
||||
showCustomDetailsModal(model) {
|
||||
const modalName = (model.action_name + '_details').replace(/\_/g, "-");
|
||||
|
||||
showModal(modalName, {
|
||||
model,
|
||||
admin: true,
|
||||
templateName: 'site-customization-change'
|
||||
});
|
||||
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
|
||||
let modal = showModal('admin-theme-change', { model, admin: true});
|
||||
this.controllerFor('modal').set('modalClass', 'history-modal');
|
||||
modal.loadDiff();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -7,11 +7,11 @@
|
||||
@module Discourse
|
||||
**/
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: { mode: {}, "start-date": {}, "end-date": {}, "category-id": {}, "group-id": {}},
|
||||
queryParams: { mode: {}, "start_date": {}, "end_date": {}, "category_id": {}, "group_id": {} },
|
||||
|
||||
model: function(params) {
|
||||
const Report = require('admin/models/report').default;
|
||||
return Report.find(params.type, params['start-date'], params['end-date'], params['category-id'], params['group-id']);
|
||||
return Report.find(params.type, params['start_date'], params['end_date'], params['category_id'], params['group_id']);
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
|
||||
@ -15,10 +15,14 @@ export default function() {
|
||||
});
|
||||
|
||||
this.route('adminCustomize', { path: '/customize', resetNamespace: true } ,function() {
|
||||
this.route('colors');
|
||||
|
||||
this.route('adminCustomizeCssHtml', { path: 'css_html', resetNamespace: true }, function() {
|
||||
this.route('show', {path: '/:site_customization_id/:section'});
|
||||
this.route('colors', function() {
|
||||
this.route('show', {path: '/:scheme_id'});
|
||||
});
|
||||
|
||||
this.route('adminCustomizeThemes', { path: 'themes', resetNamespace: true }, function() {
|
||||
this.route('show', {path: '/:theme_id'});
|
||||
this.route('edit', {path: '/:theme_id/:target/:field_name/edit'});
|
||||
});
|
||||
|
||||
this.route('adminSiteText', { path: '/site_texts', resetNamespace: true }, function() {
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
<li>
|
||||
<a href="/admin/customize/css_html/{{customization.id}}/css" class="{{if active 'active'}}">
|
||||
{{customization.description}}
|
||||
</a>
|
||||
</li>
|
||||
@ -0,0 +1,8 @@
|
||||
<label class='checkbox-label'>
|
||||
{{input type="checkbox" disabled=disabled checked=checkedInternal}}
|
||||
{{label}}
|
||||
</label>
|
||||
{{#if changed}}
|
||||
{{d-button action="finished" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelled" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
@ -0,0 +1 @@
|
||||
<p class="about">{{i18n 'admin.customize.colors.about'}}</p>
|
||||
@ -0,0 +1,62 @@
|
||||
<div class="color-scheme show-current-style">
|
||||
<div class="admin-container">
|
||||
<h1>{{#if model.theme_id}}{{model.name}}{{else}}{{text-field class="style-name" value=model.name}}{{/if}}</h1>
|
||||
<div class="controls">
|
||||
{{#unless model.theme_id}}
|
||||
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button>
|
||||
{{/unless}}
|
||||
<button {{action "copy" model}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button>
|
||||
<button {{action "copyToClipboard" model}} class='btn'><i class="fa fa-clipboard"></i> {{i18n 'admin.customize.copy_to_clipboard'}}</button>
|
||||
{{#if model.theme_id}}
|
||||
{{i18n "admin.customize.theme_owner"}}
|
||||
{{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}}
|
||||
{{else}}
|
||||
<button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button>
|
||||
{{/if}}
|
||||
<span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
<table class="table colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="hex">{{i18n 'admin.customize.color'}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each colors as |c|}}
|
||||
<tr class="{{if c.changed 'changed'}} {{if c.valid 'valid' 'invalid'}}">
|
||||
<td class="name" title={{c.name}}>
|
||||
<b>{{c.translatedName}}</b>
|
||||
<br/>
|
||||
<span class="description">{{c.description}}</span>
|
||||
</td>
|
||||
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
|
||||
<td class="actions">
|
||||
{{#unless model.theme_id}}
|
||||
<button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button>
|
||||
<button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button>
|
||||
{{/unless}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,78 +1,17 @@
|
||||
<div class='content-list span6'>
|
||||
<div class='content-list span6 color-schemes'>
|
||||
<h3>{{i18n 'admin.customize.colors.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each model as |scheme|}}
|
||||
{{#unless scheme.is_base}}
|
||||
<li><a {{action "selectColorScheme" scheme}} class="{{if scheme.selected 'active'}}">{{scheme.description}}</a></li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{fa-icon 'paint-brush'}}{{scheme.description}}{{/link-to}}
|
||||
</li>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
<button {{action "newColorScheme"}} class='btn'><i class="fa fa-plus"></i>{{i18n 'admin.customize.new'}}</button>
|
||||
<button {{action "newColorScheme"}} class='btn'>{{fa-icon 'plus'}}{{i18n 'admin.customize.new'}}</button>
|
||||
</div>
|
||||
|
||||
{{#if selectedItem}}
|
||||
<div class="current-style color-scheme">
|
||||
<div class="admin-container">
|
||||
<h1>{{text-field class="style-name" value=selectedItem.name}}</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button {{action "save"}} disabled={{selectedItem.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button>
|
||||
<button {{action "toggleEnabled"}} disabled={{selectedItem.disableEnable}} class="btn">
|
||||
{{#if selectedItem.enabled}}
|
||||
{{i18n 'disable'}}
|
||||
{{else}}
|
||||
{{i18n 'enable'}}
|
||||
{{/if}}
|
||||
</button>
|
||||
<button {{action "copy" selectedItem}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button>
|
||||
<button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button>
|
||||
<span class="saving {{unless selectedItem.savingStatus 'hidden'}}">{{selectedItem.savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
<table class="table colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="hex">{{i18n 'admin.customize.color'}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each colors as |c|}}
|
||||
<tr class="{{if c.changed 'changed'}} {{if c.valid 'valid' 'invalid'}}">
|
||||
<td class="name" title={{c.name}}>
|
||||
<b>{{c.translatedName}}</b>
|
||||
<br/>
|
||||
<span class="description">{{c.description}}</span>
|
||||
</td>
|
||||
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
|
||||
<td class="actions">
|
||||
<button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button>
|
||||
<button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="about">{{i18n 'admin.customize.colors.about'}}</p>
|
||||
{{/if}}
|
||||
{{outlet}}
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
<div class="current-style {{if maximized 'maximized'}}">
|
||||
<div class='wrapper'>
|
||||
{{text-field class="style-name" value=model.name}}
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<ul class="nav nav-pills">
|
||||
{{#if mobile}}
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-css' replace=true}}{{i18n "admin.customize.css"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-header' replace=true}}{{i18n "admin.customize.header"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-top' replace=true}}{{i18n "admin.customize.top"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-footer' replace=true}}{{i18n "admin.customize.footer"}}{{/link-to}}</li>
|
||||
{{else}}
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'css' replace=true}}{{i18n "admin.customize.css"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'header' replace=true}}{{i18n "admin.customize.header"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'top' replace=true}}{{i18n "admin.customize.top"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'footer' replace=true}}{{i18n "admin.customize.footer"}}{{/link-to}}</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeCssHtml.show' model.id 'head-tag'}}
|
||||
{{fa-icon "file-text-o"}} {{i18n 'admin.customize.head_tag.text'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeCssHtml.show' model.id 'body-tag'}}
|
||||
{{fa-icon "file-text-o"}} {{i18n 'admin.customize.body_tag.text'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'embedded-css' replace=true}}{{i18n "admin.customize.embedded_css"}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
<li class='toggle-mobile'>
|
||||
<a class="{{if mobile 'active'}}" {{action "toggleMobile"}}>{{fa-icon "mobile"}}</a>
|
||||
</li>
|
||||
<li class='toggle-maximize'>
|
||||
<a {{action "toggleMaximize"}}>
|
||||
<i class="fa fa-{{maximizeIcon}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admin-container">
|
||||
{{#if cssActive}}{{ace-editor content=model.stylesheet mode="scss"}}{{/if}}
|
||||
{{#if headerActive}}{{ace-editor content=model.header mode="html"}}{{/if}}
|
||||
{{#if topActive}}{{ace-editor content=model.top mode="html"}}{{/if}}
|
||||
{{#if footerActive}}{{ace-editor content=model.footer mode="html"}}{{/if}}
|
||||
{{#if headTagActive}}{{ace-editor content=model.head_tag mode="html"}}{{/if}}
|
||||
{{#if bodyTagActive}}{{ace-editor content=model.body_tag mode="html"}}{{/if}}
|
||||
{{#if embeddedCssActive}}{{ace-editor content=model.embedded_css mode="css"}}{{/if}}
|
||||
{{#if mobileCssActive}}{{ace-editor content=model.mobile_stylesheet mode="scss"}}{{/if}}
|
||||
{{#if mobileHeaderActive}}{{ace-editor content=model.mobile_header mode="html"}}{{/if}}
|
||||
{{#if mobileTopActive}}{{ace-editor content=model.mobile_top mode="html"}}{{/if}}
|
||||
{{#if mobileFooterActive}}{{ace-editor content=model.mobile_footer mode="html"}}{{/if}}
|
||||
</div>
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='status-actions'>
|
||||
<span>{{i18n 'admin.customize.enabled'}} {{input type="checkbox" checked=model.enabled}}</span>
|
||||
{{#unless model.changed}}
|
||||
<a class='preview-link' href={{previewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_preview'}}">{{i18n 'admin.customize.preview'}}</a>
|
||||
|
|
||||
<a href={{undoPreviewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_undo_preview'}}">{{i18n 'admin.customize.undo_preview'}}</a>
|
||||
|
|
||||
<a href={{defaultStyleUrl}} target='_blank' title="{{i18n 'admin.customize.explain_rescue_preview'}}">{{i18n 'admin.customize.rescue_preview'}}</a><br>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class='buttons'>
|
||||
{{#d-button action="save" disabled=saveDisabled class='btn-primary'}}
|
||||
{{saveButtonText}}
|
||||
{{/d-button}}
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,13 +0,0 @@
|
||||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.customize.css_html.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each model as |c|}}
|
||||
{{customize-link customization=c}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button label="admin.customize.new" icon="plus" action="newCustomization" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
||||
@ -0,0 +1,80 @@
|
||||
<div class="current-style {{if maximized 'maximized'}}">
|
||||
<div class='wrapper'>
|
||||
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
|
||||
|
||||
{{#if error}}
|
||||
<pre class='field-error'>{{error}}</pre>
|
||||
{{/if}}
|
||||
|
||||
<div class='edit-main-nav'>
|
||||
<ul class='nav nav-pills target'>
|
||||
{{#if showCommon}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.common'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showDesktop}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{fa-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showMobile}}
|
||||
<li class='mobile'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{fa-icon 'mobile'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<div class='show-overidden'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<ul class='nav nav-pills fields'>
|
||||
{{#each fields as |field|}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id currentTargetName field.name replace=true title=field.title}}
|
||||
{{#if field.icon}}{{fa-icon field.icon}} {{/if}}
|
||||
{{i18n field.key}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
<li class='toggle-maximize'>
|
||||
<a {{action "toggleMaximize"}}>
|
||||
<i class="fa fa-{{maximizeIcon}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}
|
||||
</div>
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='status-actions'>
|
||||
{{#unless model.changed}}
|
||||
<a class='preview-link' href={{previewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_preview'}}">{{i18n 'admin.customize.preview'}}</a>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class='buttons'>
|
||||
{{#d-button action="save" disabled=saveDisabled class='btn-primary'}}
|
||||
{{saveButtonText}}
|
||||
{{/d-button}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
121
app/assets/javascripts/admin/templates/customize-themes-show.hbs
Normal file
121
app/assets/javascripts/admin/templates/customize-themes-show.hbs
Normal file
@ -0,0 +1,121 @@
|
||||
<div class="show-current-style">
|
||||
<h2>
|
||||
{{#if editingName}}
|
||||
{{text-field value=model.name autofocus="true"}}
|
||||
{{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelEditingName" class="btn-small cancel-edit" icon="times"}}
|
||||
{{else}}
|
||||
{{model.name}} <a {{action "startEditingName"}}>{{fa-icon "pencil"}}</a>
|
||||
{{/if}}
|
||||
</h2>
|
||||
|
||||
{{#if model.remote_theme}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
|
||||
</p>
|
||||
{{#if model.remote_theme.license_url}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{fa-icon "copyright"}}</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if parentThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
|
||||
<ul>
|
||||
{{#each parentThemes as |theme|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
</p>
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
|
||||
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
|
||||
<p>{{combo-box content=colorSchemes
|
||||
nameProperty="name"
|
||||
value=colorSchemeId
|
||||
selectionIcon="paint-brush"
|
||||
valueAttribute="id"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
{{/if}}
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
|
||||
{{else}}
|
||||
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||
{{#if model.remote_theme}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
{{i18n 'admin.customize.theme.updating'}}
|
||||
{{else}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||
{{else}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if availableChildThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
<p>
|
||||
<label class='checkbox-label'>
|
||||
{{input type="checkbox" checked=allowChildThemes}}
|
||||
{{i18n "admin.customize.theme.child_themes_check"}}
|
||||
</label>
|
||||
</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/unless}}
|
||||
{{#if selectableChildThemes}}
|
||||
<p>{{combo-box content=selectableChildThemes
|
||||
nameProperty="name"
|
||||
value=selectedChildThemeId
|
||||
valueAttribute="id"}}
|
||||
|
||||
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<a href='{{previewUrl}}' title="{{i18n 'admin.customize.explain_preview'}}" target='_blank' class='btn'>{{fa-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}}</a>
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
||||
24
app/assets/javascripts/admin/templates/customize-themes.hbs
Normal file
24
app/assets/javascripts/admin/templates/customize-themes.hbs
Normal file
@ -0,0 +1,24 @@
|
||||
{{#unless editingTheme}}
|
||||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each sortedThemes as |theme|}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
|
||||
{{theme.name}}
|
||||
{{#if theme.user_selectable}}
|
||||
{{fa-icon "user"}}
|
||||
{{/if}}
|
||||
{{#if theme.default}}
|
||||
{{fa-icon "asterisk"}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button label="admin.customize.new" icon="plus" action="newTheme" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{outlet}}
|
||||
@ -1,7 +1,7 @@
|
||||
<div class='customize'>
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
|
||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||
{{nav-item route='adminCustomizeCssHtml' label='admin.customize.css_html.title'}}
|
||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||
|
||||
@ -83,6 +83,12 @@
|
||||
{{combo-box name="alias" valueAttribute="value" value=model.alias_level content=aliasLevelOptions}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{i18n 'groups.notification_level'}}</label>
|
||||
{{notifications-button i18nPrefix='groups.notifications' notificationLevel=model.default_notification_level}}
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label for="automatic_membership">{{i18n 'admin.groups.automatic_membership_email_domains'}}</label>
|
||||
|
||||
@ -1 +1,11 @@
|
||||
<p>{{i18n "admin.groups.bulk_complete"}}</p>
|
||||
{{#if bulkAddResponse}}
|
||||
<p>{{{bulkAddResponse.message}}}</p>
|
||||
{{#if bulkAddResponse.users_not_added}}
|
||||
<p>{{i18n "admin.groups.bulk_complete_users_not_added"}}</p>
|
||||
{{#each bulkAddResponse.users_not_added as |user|}}
|
||||
{{user}}<br/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>{{i18n "admin.groups.bulk_complete"}}</p>
|
||||
{{/if}}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
<div>
|
||||
{{#d-modal-body title="admin.customize.colors.select_base.title"}}
|
||||
{{i18n "admin.customize.colors.select_base.description"}}
|
||||
{{combo-box content=model
|
||||
nameProperty="name"
|
||||
value=selectedBaseThemeId
|
||||
valueAttribute="base_scheme_id"}}
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "selectBase"}}>{{fa-icon 'plus'}}{{i18n 'admin.customize.new'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,27 @@
|
||||
{{#d-modal-body class='upload-selector' title="admin.customize.theme.import_theme"}}
|
||||
<div class="radios">
|
||||
{{radio-button name="upload" id="local" value="local" selection=selection}}
|
||||
<label class="radio" for="local">{{i18n 'upload_selector.from_my_computer'}}</label>
|
||||
{{#if local}}
|
||||
<div class="inputs">
|
||||
<input type="file" id="file-input" accept='.dcstyle.json'><br>
|
||||
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="radios">
|
||||
{{radio-button name="upload" id="remote" value="remote" selection=selection}}
|
||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||
{{#if remote}}
|
||||
<div class="inputs">
|
||||
{{input value=uploadUrl placeholder="https://github.com/discourse/discourse/sample_theme"}}
|
||||
<span class="description">{{i18n 'admin.customize.theme.import_web_tip'}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="importTheme" disabled=loading class='btn btn-primary' icon='upload' label='admin.customize.import'}}
|
||||
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
</div>
|
||||
@ -0,0 +1,8 @@
|
||||
<div>
|
||||
{{#d-modal-body title="admin.logs.staff_actions.modal_title"}}
|
||||
{{{diff}}}
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,29 +0,0 @@
|
||||
<div>
|
||||
<ul class="nav nav-pills">
|
||||
<li class="{{if newSelected 'active'}}">
|
||||
<a href {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a>
|
||||
</li>
|
||||
<li class="{{if previousSelected 'active'}}">
|
||||
<a href {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{#d-modal-body title="admin.logs.staff_actions.modal_title"}}
|
||||
<div class="modal-tab new-tab {{unless newSelected 'invisible'}}">
|
||||
{{#if model.new_value}}
|
||||
{{site-customization-change-details change=model.new_value}}
|
||||
{{else}}
|
||||
{{i18n 'admin.logs.staff_actions.deleted'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-tab previous-tab {{unless previousSelected 'invisible'}}">
|
||||
{{#if model.previous_value}}
|
||||
{{site-customization-change-details change=model.previous_value}}
|
||||
{{else}}
|
||||
{{i18n 'admin.logs.staff_actions.no_previous'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -343,32 +343,31 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class='details'>
|
||||
<h1>{{i18n 'admin.groups.title'}}</h1>
|
||||
|
||||
{{#if currentUser.admin}}
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.automatic'}}</div>
|
||||
<div class='value'>{{automaticGroups}}</div>
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
|
||||
<div class='value'>
|
||||
{{admin-group-selector selected=model.customGroups available=availableGroups}}
|
||||
{{#if currentUser.admin}}
|
||||
<section class='details'>
|
||||
<h1>{{i18n 'admin.groups.title'}}</h1>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.automatic'}}</div>
|
||||
<div class='value'>{{automaticGroups}}</div>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#if model.customGroups}}
|
||||
{{i18n 'admin.groups.primary'}}
|
||||
{{combo-box content=model.customGroups value=model.primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
|
||||
{{/if}}
|
||||
{{#if primaryGroupDirty}}
|
||||
{{d-button icon="check" class="ok" action="savePrimaryGroup"}}
|
||||
{{d-button icon="times" class="cancel" action="resetPrimaryGroup"}}
|
||||
{{/if}}
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
|
||||
<div class='value'>
|
||||
{{admin-group-selector selected=model.customGroups available=availableGroups}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#if model.customGroups}}
|
||||
{{i18n 'admin.groups.primary'}}
|
||||
{{combo-box content=model.customGroups value=model.primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
|
||||
{{/if}}
|
||||
{{#if primaryGroupDirty}}
|
||||
{{d-button icon="check" class="ok" action="savePrimaryGroup"}}
|
||||
{{d-button icon="times" class="cancel" action="resetPrimaryGroup"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class='details'>
|
||||
<h1>{{i18n 'admin.user.activity'}}</h1>
|
||||
|
||||
@ -11,28 +11,53 @@ export default Ember.Component.extend(bufferedRender({
|
||||
buildBuffer(buffer) {
|
||||
const nameProperty = this.get('nameProperty');
|
||||
const none = this.get('none');
|
||||
let noneValue = null;
|
||||
|
||||
// Add none option if required
|
||||
if (typeof none === "string") {
|
||||
buffer.push('<option value="">' + I18n.t(none) + "</option>");
|
||||
} else if (typeof none === "object") {
|
||||
buffer.push("<option value=\"\">" + Em.get(none, nameProperty) + "</option>");
|
||||
noneValue = Em.get(none, this.get('valueAttribute'));
|
||||
buffer.push(`<option value="${noneValue}">${Em.get(none, nameProperty)}</option>`);
|
||||
}
|
||||
|
||||
let selected = this.get('value');
|
||||
if (!Em.isNone(selected)) { selected = selected.toString(); }
|
||||
|
||||
if (this.get('content')) {
|
||||
this.get('content').forEach(o => {
|
||||
let selectedFound = false;
|
||||
let firstVal = undefined;
|
||||
const content = this.get('content');
|
||||
|
||||
if (content) {
|
||||
let first = true;
|
||||
content.forEach(o => {
|
||||
let val = o[this.get('valueAttribute')];
|
||||
if (typeof val === "undefined") { val = o; }
|
||||
if (!Em.isNone(val)) { val = val.toString(); }
|
||||
|
||||
const selectedText = (val === selected) ? "selected" : "";
|
||||
const name = Handlebars.Utils.escapeExpression(Ember.get(o, nameProperty) || o);
|
||||
|
||||
if (val === selected) {
|
||||
selectedFound = true;
|
||||
}
|
||||
if (first) {
|
||||
firstVal = val;
|
||||
first = false;
|
||||
}
|
||||
buffer.push(`<option ${selectedText} value="${val}">${name}</option>`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedFound && !noneValue) {
|
||||
if (none) {
|
||||
this.set('value', null);
|
||||
} else {
|
||||
this.set('value', firstVal);
|
||||
}
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._updateSelect2);
|
||||
},
|
||||
|
||||
@observes('value')
|
||||
@ -66,12 +91,31 @@ export default Ember.Component.extend(bufferedRender({
|
||||
|
||||
const $elem = this.$();
|
||||
const caps = this.capabilities;
|
||||
const minimumResultsForSearch = (caps && caps.isIOS) ? -1 : 5;
|
||||
$elem.select2({
|
||||
formatResult: this.comboTemplate, minimumResultsForSearch,
|
||||
const minimumResultsForSearch = this.get('minimumResultsForSearch') || ((caps && caps.isIOS) ? -1 : 5);
|
||||
|
||||
if (!this.get("selectionTemplate") && this.get("selectionIcon")) {
|
||||
this.selectionTemplate = (item) => {
|
||||
let name = Em.get(item, 'text');
|
||||
name = Handlebars.escapeExpression(name);
|
||||
return `<i class='fa fa-${this.get("selectionIcon")}'></i>${name}`;
|
||||
};
|
||||
}
|
||||
|
||||
const options = {
|
||||
minimumResultsForSearch,
|
||||
width: this.get('width') || 'resolve',
|
||||
allowClear: true
|
||||
});
|
||||
};
|
||||
|
||||
if (this.comboTemplate) {
|
||||
options.formatResult = this.comboTemplate.bind(this);
|
||||
}
|
||||
|
||||
if (this.selectionTemplate) {
|
||||
options.formatSelection = this.selectionTemplate.bind(this);
|
||||
}
|
||||
|
||||
$elem.select2(options);
|
||||
|
||||
const castInteger = this.get('castInteger');
|
||||
$elem.on("change", e => {
|
||||
@ -81,9 +125,14 @@ export default Ember.Component.extend(bufferedRender({
|
||||
}
|
||||
this.set('value', val);
|
||||
});
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._triggerChange);
|
||||
},
|
||||
|
||||
_updateSelect2() {
|
||||
this.$().trigger('change.select2');
|
||||
},
|
||||
|
||||
_triggerChange() {
|
||||
this.$().trigger('change');
|
||||
},
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host', 'web-hook', 'web-hook-event'];
|
||||
const ADMIN_MODELS = ['plugin', 'theme', 'embeddable-host', 'web-hook', 'web-hook-event'];
|
||||
|
||||
export function Result(payload, responseJson) {
|
||||
this.payload = payload;
|
||||
@ -76,22 +76,38 @@ export default Ember.Object.extend({
|
||||
this.cached[this.storageKey(type,findArgs,opts)] = hydrated;
|
||||
},
|
||||
|
||||
jsonMode: false,
|
||||
|
||||
getPayload(method, data) {
|
||||
let payload = {method, data};
|
||||
|
||||
if (this.jsonMode) {
|
||||
payload.contentType = "application/json";
|
||||
payload.data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
update(store, type, id, attrs) {
|
||||
const data = {};
|
||||
const typeField = Ember.String.underscore(type);
|
||||
data[typeField] = attrs;
|
||||
return ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
|
||||
return ajax(this.pathFor(store, type, id), this.getPayload('PUT', data))
|
||||
.then(function(json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
},
|
||||
|
||||
createRecord(store, type, attrs) {
|
||||
const data = {};
|
||||
const typeField = Ember.String.underscore(type);
|
||||
data[typeField] = attrs;
|
||||
return ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
return ajax(this.pathFor(store, type), this.getPayload('POST', data))
|
||||
.then(function (json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
},
|
||||
|
||||
destroyRecord(store, type, record) {
|
||||
|
||||
@ -0,0 +1,172 @@
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import Combobox from 'discourse-common/components/combo-box';
|
||||
import { CLOSE_STATUS_TYPE } from 'discourse/controllers/edit-topic-status-update';
|
||||
|
||||
const LATER_TODAY = 'later_today';
|
||||
const TOMORROW = 'tomorrow';
|
||||
const LATER_THIS_WEEK = 'later_this_week';
|
||||
const THIS_WEEKEND = 'this_weekend';
|
||||
const NEXT_WEEK = 'next_week';
|
||||
export const PICK_DATE_AND_TIME = 'pick_date_and_time';
|
||||
export const SET_BASED_ON_LAST_POST = 'set_based_on_last_post';
|
||||
|
||||
export const FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export default Combobox.extend({
|
||||
classNames: ['auto-update-input-selector'],
|
||||
isCustom: Ember.computed.equal("value", PICK_DATE_AND_TIME),
|
||||
|
||||
@computed()
|
||||
content() {
|
||||
const selections = [];
|
||||
const now = moment();
|
||||
const canScheduleToday = (24 - now.hour()) > 6;
|
||||
const day = now.day();
|
||||
|
||||
if (canScheduleToday) {
|
||||
selections.push({
|
||||
id: LATER_TODAY,
|
||||
name: I18n.t('topic.auto_update_input.later_today')
|
||||
});
|
||||
}
|
||||
|
||||
selections.push({
|
||||
id: TOMORROW,
|
||||
name: I18n.t('topic.auto_update_input.tomorrow')
|
||||
});
|
||||
|
||||
if (!canScheduleToday && day < 4) {
|
||||
selections.push({
|
||||
id: LATER_THIS_WEEK,
|
||||
name: I18n.t('topic.auto_update_input.later_this_week')
|
||||
});
|
||||
}
|
||||
|
||||
if (day < 5) {
|
||||
selections.push({
|
||||
id: THIS_WEEKEND,
|
||||
name: I18n.t('topic.auto_update_input.this_weekend')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (day !== 7) {
|
||||
selections.push({
|
||||
id: NEXT_WEEK,
|
||||
name: I18n.t('topic.auto_update_input.next_week')
|
||||
});
|
||||
}
|
||||
|
||||
selections.push({
|
||||
id: PICK_DATE_AND_TIME,
|
||||
name: I18n.t('topic.auto_update_input.pick_date_and_time')
|
||||
});
|
||||
|
||||
if (this.get('statusType') === CLOSE_STATUS_TYPE) {
|
||||
selections.push({
|
||||
id: SET_BASED_ON_LAST_POST,
|
||||
name: I18n.t('topic.auto_update_input.set_based_on_last_post')
|
||||
});
|
||||
}
|
||||
|
||||
return selections;
|
||||
},
|
||||
|
||||
@observes('value')
|
||||
_updateInput() {
|
||||
if (this.get('isCustom')) return;
|
||||
let input = null;
|
||||
const { time } = this.get('updateAt');
|
||||
|
||||
if (time && !Ember.isEmpty(this.get('value'))) {
|
||||
input = time.format(FORMAT);
|
||||
}
|
||||
|
||||
this.set('input', input);
|
||||
},
|
||||
|
||||
@computed('value')
|
||||
updateAt(value) {
|
||||
return this._updateAt(value);
|
||||
},
|
||||
|
||||
comboTemplate(state) {
|
||||
return this._format(state);
|
||||
},
|
||||
|
||||
selectionTemplate(state) {
|
||||
return this._format(state);
|
||||
},
|
||||
|
||||
_format(state) {
|
||||
let { time, icon } = this._updateAt(state.id);
|
||||
let icons;
|
||||
|
||||
if (icon) {
|
||||
icons = icon.split(',').map(i => {
|
||||
return `<i class='fa fa-${i}'/>`;
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
if (time) {
|
||||
if (state.id === LATER_TODAY) {
|
||||
time = time.format('hh:mm a');
|
||||
} else {
|
||||
time = time.format('ddd, hh:mm a');
|
||||
}
|
||||
}
|
||||
|
||||
let output = "";
|
||||
|
||||
if (!Ember.isEmpty(icons)) {
|
||||
output += `<span class='auto-update-input-selector-icons'>${icons}</span>`;
|
||||
}
|
||||
|
||||
output += `<span>${state.text}</span>`;
|
||||
|
||||
if (time) {
|
||||
output += `<span class='auto-update-input-selector-datetime'>${time}</span>`;
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
_updateAt(selection) {
|
||||
let time = moment();
|
||||
let icon;
|
||||
const timeOfDay = this.get('statusType') !== CLOSE_STATUS_TYPE ? 8 : 18;
|
||||
|
||||
switch(selection) {
|
||||
case LATER_TODAY:
|
||||
time = time.hour(18).minute(0);
|
||||
icon = 'desktop';
|
||||
break;
|
||||
case TOMORROW:
|
||||
time = time.add(1, 'day').hour(timeOfDay).minute(0);
|
||||
icon = 'sun-o';
|
||||
break;
|
||||
case LATER_THIS_WEEK:
|
||||
time = time.add(2, 'day').hour(timeOfDay).minute(0);
|
||||
icon = 'briefcase';
|
||||
break;
|
||||
case THIS_WEEKEND:
|
||||
time = time.day(6).hour(timeOfDay).minute(0);
|
||||
icon = 'bed';
|
||||
break;
|
||||
case NEXT_WEEK:
|
||||
time = time.add(1, 'week').day(1).hour(timeOfDay).minute(0);
|
||||
icon = 'briefcase';
|
||||
break;
|
||||
case PICK_DATE_AND_TIME:
|
||||
time = null;
|
||||
icon = 'calendar-plus-o';
|
||||
break;
|
||||
case SET_BASED_ON_LAST_POST:
|
||||
time = null;
|
||||
icon = 'clock-o';
|
||||
break;
|
||||
}
|
||||
|
||||
return { time, icon };
|
||||
},
|
||||
});
|
||||
@ -1,47 +1,92 @@
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
FORMAT,
|
||||
PICK_DATE_AND_TIME,
|
||||
SET_BASED_ON_LAST_POST
|
||||
} from "discourse/components/auto-update-input-selector";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
limited: false,
|
||||
selection: null,
|
||||
date: null,
|
||||
time: null,
|
||||
isCustom: Ember.computed.equal('selection', PICK_DATE_AND_TIME),
|
||||
isBasedOnLastPost: Ember.computed.equal('selection', SET_BASED_ON_LAST_POST),
|
||||
|
||||
didInsertElement() {
|
||||
init() {
|
||||
this._super();
|
||||
this._updateInputValid();
|
||||
},
|
||||
|
||||
@computed("limited")
|
||||
inputUnitsKey(limited) {
|
||||
return limited ? "topic.auto_update_input.limited.units" : "topic.auto_update_input.all.units";
|
||||
},
|
||||
const input = this.get('input');
|
||||
|
||||
@computed("limited")
|
||||
inputExamplesKey(limited) {
|
||||
return limited ? "topic.auto_update_input.limited.examples" : "topic.auto_update_input.all.examples";
|
||||
},
|
||||
|
||||
@observes("input", "limited")
|
||||
_updateInputValid() {
|
||||
this.set(
|
||||
"inputValid", this._isInputValid(this.get("input"), this.get("limited"))
|
||||
);
|
||||
},
|
||||
|
||||
_isInputValid(input, limited) {
|
||||
const t = (input || "").toString().trim();
|
||||
|
||||
if (t.length === 0) {
|
||||
return true;
|
||||
// "empty" is always valid
|
||||
} else if (limited) {
|
||||
// only # of hours in limited mode
|
||||
return t.match(/^(\d+\.)?\d+$/);
|
||||
} else {
|
||||
if (t.match(/^\d{4}-\d{1,2}-\d{1,2}(?: \d{1,2}:\d{2}(\s?[AP]M)?){0,1}$/i)) {
|
||||
// timestamp must be in the future
|
||||
return moment(t).isAfter();
|
||||
if (input) {
|
||||
if (this.get('basedOnLastPost')) {
|
||||
this.set('selection', SET_BASED_ON_LAST_POST);
|
||||
} else {
|
||||
// either # of hours or absolute time
|
||||
return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null;
|
||||
this.set('selection', PICK_DATE_AND_TIME);
|
||||
const datetime = moment(input);
|
||||
this.set('date', datetime.toDate());
|
||||
this.set('time', datetime.format("HH:mm"));
|
||||
this._updateInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@observes("date", "time")
|
||||
_updateInput() {
|
||||
const date = moment(this.get('date')).format("YYYY-MM-DD");
|
||||
const time = (this.get('time') && ` ${this.get('time')}`) || '';
|
||||
this.set('input', moment(`${date}${time}`).format(FORMAT));
|
||||
},
|
||||
|
||||
@observes("isBasedOnLastPost")
|
||||
_updateBasedOnLastPost() {
|
||||
this.set('basedOnLastPost', this.get('isBasedOnLastPost'));
|
||||
},
|
||||
|
||||
@computed("input", "isBasedOnLastPost")
|
||||
duration(input, isBasedOnLastPost) {
|
||||
const now = moment();
|
||||
|
||||
if (isBasedOnLastPost) {
|
||||
return parseFloat(input);
|
||||
} else {
|
||||
return moment(input) - now;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("input", "isBasedOnLastPost")
|
||||
executeAt(input, isBasedOnLastPost) {
|
||||
if (isBasedOnLastPost) {
|
||||
return moment().add(input, 'hours').format(FORMAT);
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately")
|
||||
showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately) {
|
||||
if (!statusType || willCloseImmediately) return false;
|
||||
|
||||
if (isCustom) {
|
||||
return date || time;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
|
||||
@computed('isBasedOnLastPost', 'input', 'lastPostedAt')
|
||||
willCloseImmediately(isBasedOnLastPost, input, lastPostedAt) {
|
||||
if (isBasedOnLastPost && input) {
|
||||
let closeDate = moment(lastPostedAt);
|
||||
closeDate = closeDate.add(input, 'hours');
|
||||
return closeDate < moment();
|
||||
}
|
||||
},
|
||||
|
||||
@computed('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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,37 +1,14 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { sanitize, emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
size: 'medium',
|
||||
classNameBindings: [':badge-card', 'size', 'badge.slug', 'navigateOnClick:hyperlink'],
|
||||
|
||||
click(e){
|
||||
if (e.target && e.target.nodeName === "A") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.get('navigateOnClick')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var url = this.get('badge.url');
|
||||
const username = this.get('username');
|
||||
if (username) {
|
||||
url = url + "?username=" + encodeURIComponent(username);
|
||||
}
|
||||
DiscourseURL.routeTo(url);
|
||||
return true;
|
||||
},
|
||||
classNameBindings: [':badge-card', 'size', 'badge.slug'],
|
||||
|
||||
@computed('count', 'badge.grant_count')
|
||||
displayCount(count, grantCount) {
|
||||
if (count == null) {
|
||||
return grantCount;
|
||||
}
|
||||
if (count > 1) {
|
||||
return count;
|
||||
}
|
||||
if (count == null) { return grantCount; }
|
||||
if (count > 1) { return count; }
|
||||
},
|
||||
|
||||
@computed('size')
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import ComboboxView from 'discourse-common/components/combo-box';
|
||||
import Combobox from 'discourse-common/components/combo-box';
|
||||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
import PermissionType from 'discourse/models/permission-type';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
export default ComboboxView.extend({
|
||||
export default Combobox.extend({
|
||||
classNames: ['combobox category-combobox'],
|
||||
dataAttributes: ['id', 'description_text'],
|
||||
overrideWidths: true,
|
||||
|
||||
@ -45,7 +45,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@observes('composer.replyLength')
|
||||
_clearFeaturedLink() {
|
||||
if (this.get('watchForLink') && this.get('composer.replyLength') === 0) {
|
||||
if (this.get('watchForLink') && this.bodyIsDefault()) {
|
||||
this.set('composer.featuredLink', null);
|
||||
}
|
||||
},
|
||||
@ -53,7 +53,7 @@ export default Ember.Component.extend({
|
||||
_checkForUrl() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
|
||||
|
||||
if (this.get('isAbsoluteUrl') && (this.get('composer.reply')||"").length === 0) {
|
||||
if (this.get('isAbsoluteUrl') && this.bodyIsDefault()) {
|
||||
|
||||
// only feature links to external sites
|
||||
if (this.get('composer.title').match(new RegExp("^https?:\\/\\/" + window.location.hostname, "i"))) { return; }
|
||||
@ -88,9 +88,10 @@ export default Ember.Component.extend({
|
||||
this.set('composer.featuredLink', this.get('composer.title'));
|
||||
|
||||
const $h = $(html),
|
||||
heading = $h.find('h3').length > 0 ? $h.find('h3') : $h.find('h4');
|
||||
heading = $h.find('h3').length > 0 ? $h.find('h3') : $h.find('h4'),
|
||||
composer = this.get('composer');
|
||||
|
||||
this.set('composer.reply', this.get('composer.title'));
|
||||
composer.appendText(this.get('composer.title'), null, {block: true});
|
||||
|
||||
if (heading.length > 0 && heading.text().length > 0) {
|
||||
this.changeTitle(heading.text());
|
||||
@ -109,8 +110,13 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed('composer.title')
|
||||
isAbsoluteUrl() {
|
||||
return this.get('composer.titleLength') > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(this.get('composer.title'));
|
||||
@computed('composer.title', 'composer.titleLength')
|
||||
isAbsoluteUrl(title, titleLength) {
|
||||
return titleLength > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(title);
|
||||
},
|
||||
|
||||
bodyIsDefault() {
|
||||
const reply = this.get('composer.reply')||"";
|
||||
return (reply.length === 0 || (reply === (this.get("composer.category.topic_template")||"")));
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,7 +5,8 @@ export default DatePicker.extend({
|
||||
|
||||
_opts() {
|
||||
return {
|
||||
defaultDate: moment().add(1, "day").toDate(),
|
||||
defaultDate: this.get('defaultDate') || moment().add(1, "day").toDate(),
|
||||
setDefaultDate: !!this.get('defaultDate'),
|
||||
minDate: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* global Pikaday:true */
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
import { default as computed, on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Em.Component.extend({
|
||||
classNames: ["date-picker-wrapper"],
|
||||
@ -39,6 +39,11 @@ export default Em.Component.extend({
|
||||
this._picker = null;
|
||||
},
|
||||
|
||||
@computed()
|
||||
placeholder() {
|
||||
return I18n.t("dates.placeholder");
|
||||
},
|
||||
|
||||
_opts() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -6,6 +6,6 @@ export default NotificationsButton.extend({
|
||||
i18nPrefix: 'groups.notifications',
|
||||
|
||||
clicked(id) {
|
||||
this.get('group').setNotification(id);
|
||||
this.get('group').setNotification(id, this.get('user.id'));
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
|
||||
export default Em.Component.extend({
|
||||
fileInput: null,
|
||||
loading: false,
|
||||
expectedRootObjectName: null,
|
||||
hover: 0,
|
||||
|
||||
classNames: ['json-uploader'],
|
||||
|
||||
_initialize: function() {
|
||||
const $this = this.$();
|
||||
const self = this;
|
||||
|
||||
const $fileInput = $this.find('#js-file-input');
|
||||
this.set('fileInput', $fileInput[0]);
|
||||
|
||||
$fileInput.on('change', function() {
|
||||
self.fileSelected(this.files);
|
||||
});
|
||||
|
||||
$this.on('dragover', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$this.on('dragenter', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
self.set('hover', self.get('hover') + 1);
|
||||
return false;
|
||||
});
|
||||
$this.on('dragleave', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
self.set('hover', self.get('hover') - 1);
|
||||
return false;
|
||||
});
|
||||
$this.on('drop', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
self.set('hover', 0);
|
||||
self.fileSelected(e.dataTransfer.files);
|
||||
return false;
|
||||
});
|
||||
|
||||
}.on('didInsertElement'),
|
||||
|
||||
accept: function() {
|
||||
return ".json,application/json,application/x-javascript,text/json" + (this.get('extension') ? "," + this.get('extension') : "");
|
||||
}.property('extension'),
|
||||
|
||||
setReady: function() {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(this.get('value'));
|
||||
} catch (e) {
|
||||
this.set('ready', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootObject = parsed[this.get('expectedRootObjectName')];
|
||||
|
||||
if (rootObject !== null && rootObject !== undefined) {
|
||||
this.set('ready', true);
|
||||
} else {
|
||||
this.set('ready', false);
|
||||
}
|
||||
}.observes('destination', 'expectedRootObjectName'),
|
||||
|
||||
actions: {
|
||||
selectFile: function() {
|
||||
const $fileInput = $(this.get('fileInput'));
|
||||
$fileInput.click();
|
||||
}
|
||||
},
|
||||
|
||||
fileSelected(fileList) {
|
||||
const self = this;
|
||||
let files = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
files[i] = fileList[i];
|
||||
}
|
||||
const fileNameRegex = /\.(json|txt)$/;
|
||||
files = files.filter(function(file) {
|
||||
if (fileNameRegex.test(file.name)) {
|
||||
return true;
|
||||
}
|
||||
if (file.type === "text/plain") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const firstFile = fileList[0];
|
||||
|
||||
this.set('loading', true);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(evt) {
|
||||
self.set('value', evt.target.result);
|
||||
self.set('loading', false);
|
||||
};
|
||||
|
||||
reader.readAsText(firstFile);
|
||||
}
|
||||
|
||||
});
|
||||
@ -43,7 +43,7 @@ export default DropdownButton.extend({
|
||||
}
|
||||
},
|
||||
|
||||
clicked(/* id */) {
|
||||
// sub-class needs to implement this
|
||||
clicked(id) {
|
||||
this.set("notificationLevel", id);
|
||||
}
|
||||
});
|
||||
|
||||
@ -143,12 +143,14 @@ export default Em.Component.extend({
|
||||
matchRegEx = matchRegEx || replaceRegEx;
|
||||
const match = this.filterBlocks(matchRegEx);
|
||||
|
||||
let val = this.get(key);
|
||||
|
||||
if (match.length !== 0) {
|
||||
const userInput = match[0].replace(replaceRegEx, '');
|
||||
if (this.get(key) !== userInput) {
|
||||
if (val !== userInput) {
|
||||
this.set(key, userInput);
|
||||
}
|
||||
} else if(this.get(key).length !== 0) {
|
||||
} else if(val && val.length !== 0) {
|
||||
this.set(key, '');
|
||||
}
|
||||
},
|
||||
|
||||
@ -9,12 +9,14 @@ export default MountWidget.extend({
|
||||
},
|
||||
|
||||
@observes('topic.details.notification_level')
|
||||
_triggerRerender() {
|
||||
this.queueRerender();
|
||||
_triggerEvent() {
|
||||
this.appEvents.trigger('topic-notifications-button:changed', {
|
||||
type: 'notification', id: this.get('topic.details.notification_level')
|
||||
});
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button');
|
||||
this.dispatch('topic-notifications-button:changed', 'topic-notifications-button');
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,21 +2,21 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
export default Ember.Component.extend(bufferedRender({
|
||||
elementId: 'topic-status-info',
|
||||
classNames: ['topic-status-info'],
|
||||
delayedRerender: null,
|
||||
|
||||
rerenderTriggers: [
|
||||
'topic.topic_status_update',
|
||||
'topic.topic_status_update.execute_at',
|
||||
'topic.topic_status_update.based_on_last_post',
|
||||
'topic.topic_status_update.duration',
|
||||
'topic.topic_status_update.category_id',
|
||||
'statusType',
|
||||
'executeAt',
|
||||
'basedOnLastPost',
|
||||
'duration',
|
||||
'categoryId',
|
||||
],
|
||||
|
||||
buildBuffer(buffer) {
|
||||
if (!this.get('topic.topic_status_update.execute_at')) return;
|
||||
if (!this.get('executeAt')) return;
|
||||
|
||||
let statusUpdateAt = moment(this.get('topic.topic_status_update.execute_at'));
|
||||
let statusUpdateAt = moment(this.get('executeAt'));
|
||||
if (statusUpdateAt < new Date()) return;
|
||||
|
||||
let duration = moment.duration(statusUpdateAt - moment());
|
||||
@ -33,7 +33,7 @@ export default Ember.Component.extend(bufferedRender({
|
||||
rerenderDelay = 60000;
|
||||
}
|
||||
|
||||
let autoCloseHours = this.get("topic.topic_status_update.duration") || 0;
|
||||
let autoCloseHours = this.get("duration") || 0;
|
||||
|
||||
buffer.push('<h3><i class="fa fa-clock-o"></i> ');
|
||||
|
||||
@ -42,7 +42,7 @@ export default Ember.Component.extend(bufferedRender({
|
||||
duration: moment.duration(autoCloseHours, "hours").humanize(),
|
||||
};
|
||||
|
||||
const categoryId = this.get('topic.topic_status_update.category_id');
|
||||
const categoryId = this.get('categoryId');
|
||||
|
||||
if (categoryId) {
|
||||
const category = Category.findById(categoryId);
|
||||
@ -67,9 +67,9 @@ export default Ember.Component.extend(bufferedRender({
|
||||
},
|
||||
|
||||
_noticeKey() {
|
||||
const statusType = this.get('topic.topic_status_update.status_type');
|
||||
const statusType = this.get('statusType');
|
||||
|
||||
if (this.get("topic.topic_status_update.based_on_last_post")) {
|
||||
if (this.get("basedOnLastPost")) {
|
||||
return `topic.status_update_notice.auto_${statusType}_based_on_last_post`;
|
||||
} else {
|
||||
return `topic.status_update_notice.auto_${statusType}`;
|
||||
|
||||
@ -79,6 +79,6 @@ export default MountWidget.extend(Docking, {
|
||||
}
|
||||
|
||||
this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea');
|
||||
this.dispatch('topic-notifications-button:keyboard-trigger', 'topic-notifications-button');
|
||||
this.dispatch('topic-notifications-button:changed', 'topic-notifications-button');
|
||||
}
|
||||
});
|
||||
|
||||
@ -695,15 +695,21 @@ export default Ember.Controller.extend({
|
||||
|
||||
return new Ember.RSVP.Promise(function (resolve) {
|
||||
if (self.get('model.hasMetaData') || self.get('model.replyDirty')) {
|
||||
bootbox.confirm(I18n.t("post.abandon.confirm"), I18n.t("post.abandon.no_value"),
|
||||
I18n.t("post.abandon.yes_value"), function(result) {
|
||||
if (result) {
|
||||
self.destroyDraft();
|
||||
self.get('model').clearState();
|
||||
self.close();
|
||||
resolve();
|
||||
bootbox.dialog(I18n.t("post.abandon.confirm"), [
|
||||
{ label: I18n.t("post.abandon.no_value") },
|
||||
{
|
||||
label: I18n.t("post.abandon.yes_value"),
|
||||
'class': 'btn-danger',
|
||||
callback(result) {
|
||||
if (result) {
|
||||
self.destroyDraft();
|
||||
self.get('model').clearState();
|
||||
self.close();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
]);
|
||||
} else {
|
||||
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
||||
self.destroyDraft();
|
||||
|
||||
@ -3,16 +3,11 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import TopicStatusUpdate from 'discourse/models/topic-status-update';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const CLOSE_STATUS_TYPE = 'close';
|
||||
export const CLOSE_STATUS_TYPE = 'close';
|
||||
const OPEN_STATUS_TYPE = 'open';
|
||||
const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
closeStatusType: CLOSE_STATUS_TYPE,
|
||||
openStatusType: OPEN_STATUS_TYPE,
|
||||
publishToCategoryStatusType: PUBLISH_TO_CATEGORY_STATUS_TYPE,
|
||||
updateTimeValid: null,
|
||||
updateTimeInvalid: Em.computed.not('updateTimeValid'),
|
||||
loading: false,
|
||||
updateTime: null,
|
||||
topicStatusUpdate: Ember.computed.alias("model.topic_status_update"),
|
||||
@ -21,42 +16,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE),
|
||||
publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE),
|
||||
|
||||
@computed('autoClose', 'updateTime')
|
||||
disableAutoClose(autoClose, updateTime) {
|
||||
return updateTime && !autoClose;
|
||||
@computed("model.closed")
|
||||
statusUpdates(closed) {
|
||||
return [
|
||||
{ id: CLOSE_STATUS_TYPE, name: I18n.t(closed ? 'topic.temp_open.title' : 'topic.auto_close.title'), },
|
||||
{ id: OPEN_STATUS_TYPE, name: I18n.t(closed ? 'topic.auto_reopen.title' : 'topic.temp_close.title') },
|
||||
{ id: PUBLISH_TO_CATEGORY_STATUS_TYPE, name: I18n.t('topic.publish_to_category.title') }
|
||||
];
|
||||
},
|
||||
|
||||
@computed('autoOpen', 'updateTime')
|
||||
disableAutoOpen(autoOpen, updateTime) {
|
||||
return updateTime && !autoOpen;
|
||||
},
|
||||
|
||||
@computed('publishToCatgory', 'updateTime')
|
||||
disablePublishToCategory(publishToCatgory, updateTime) {
|
||||
return updateTime && !publishToCatgory;
|
||||
},
|
||||
|
||||
@computed('topicStatusUpdate.based_on_last_post', 'updateTime', 'model.last_posted_at')
|
||||
willCloseImmediately(basedOnLastPost, updateTime, lastPostedAt) {
|
||||
if (!basedOnLastPost) {
|
||||
return false;
|
||||
}
|
||||
const closeDate = new Date(lastPostedAt);
|
||||
closeDate.setHours(closeDate.getHours() + updateTime);
|
||||
return closeDate < new Date();
|
||||
},
|
||||
|
||||
@computed('topicStatusUpdate.based_on_last_post', 'model.last_posted_at')
|
||||
willCloseI18n(basedOnLastPost, lastPostedAt) {
|
||||
if (basedOnLastPost) {
|
||||
const diff = Math.round((new Date() - new Date(lastPostedAt)) / (1000*60*60));
|
||||
return I18n.t('topic.auto_close_immediate', { count: diff });
|
||||
}
|
||||
},
|
||||
|
||||
@computed('updateTime', 'updateTimeInvalid', 'loading')
|
||||
saveDisabled(updateTime, updateTimeInvalid, loading) {
|
||||
return Ember.isEmpty(updateTime) || updateTimeInvalid || loading;
|
||||
@computed('updateTime', 'loading')
|
||||
saveDisabled(updateTime, loading) {
|
||||
return Ember.isEmpty(updateTime) || loading;
|
||||
},
|
||||
|
||||
@computed("model.visible")
|
||||
@ -66,29 +37,31 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
@observes("topicStatusUpdate.execute_at", "topicStatusUpdate.duration")
|
||||
_setUpdateTime() {
|
||||
if (!this.get('topicStatusUpdate.execute_at')) return;
|
||||
|
||||
let time = null;
|
||||
|
||||
if (this.get("topicStatusUpdate.based_on_last_post")) {
|
||||
time = this.get("topicStatusUpdate.duration");
|
||||
} else if (this.get("topicStatusUpdate.execute_at")) {
|
||||
const closeTime = new Date(this.get("topicStatusUpdate.execute_at"));
|
||||
const closeTime = moment(this.get('topicStatusUpdate.execute_at'));
|
||||
|
||||
if (closeTime > new Date()) {
|
||||
time = moment(closeTime).format("YYYY-MM-DD HH:mm");
|
||||
if (closeTime > moment()) {
|
||||
time = closeTime.format("YYYY-MM-DD HH:mm");
|
||||
}
|
||||
}
|
||||
|
||||
this.set("updateTime", time);
|
||||
},
|
||||
|
||||
_setStatusUpdate(time, status_type) {
|
||||
_setStatusUpdate(time, statusType) {
|
||||
this.set('loading', true);
|
||||
|
||||
TopicStatusUpdate.updateStatus(
|
||||
this.get('model.id'),
|
||||
time,
|
||||
this.get('topicStatusUpdate.based_on_last_post'),
|
||||
status_type,
|
||||
statusType,
|
||||
this.get('categoryId')
|
||||
).then(result => {
|
||||
if (time) {
|
||||
@ -102,8 +75,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
this.set('model.closed', result.closed);
|
||||
} else {
|
||||
this.set('topicStatusUpdate', Ember.Object.create({}));
|
||||
this.set('selection', null);
|
||||
this.setProperties({
|
||||
topicStatusUpdate: Ember.Object.create({}),
|
||||
selection: null,
|
||||
updateTime: null
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
popupAjaxError(error);
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { NotificationLevels } from 'discourse/lib/notification-levels';
|
||||
import { listThemes, selectDefaultTheme, previewTheme } from 'discourse/lib/theme-selector';
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
userSelectableThemes: function(){
|
||||
return listThemes(this.site);
|
||||
}.property(),
|
||||
|
||||
@observes("selectedTheme")
|
||||
themeKeyChanged() {
|
||||
let key = this.get("selectedTheme");
|
||||
previewTheme(key);
|
||||
},
|
||||
|
||||
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
|
||||
selectedCategories(watched, tracked, muted) {
|
||||
return [].concat(watched, tracked, muted);
|
||||
@ -113,7 +124,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
{ name: I18n.t('user.auto_track_options.after_10_minutes'), value: 600000 }],
|
||||
|
||||
notificationLevelsForReplying: [{ name: I18n.t('topic.notifications.watching.title'), value: NotificationLevels.WATCHING },
|
||||
{ name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING }],
|
||||
{ name: I18n.t('topic.notifications.tracking.title'), value: NotificationLevels.TRACKING },
|
||||
{ name: I18n.t('topic.notifications.regular.title'), value: NotificationLevels.REGULAR }],
|
||||
|
||||
|
||||
considerNewTopicOptions: [{ name: I18n.t('user.new_topic_duration.not_viewed'), value: -1 },
|
||||
@ -162,6 +174,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
Discourse.User.currentProp('name', model.get('name'));
|
||||
}
|
||||
model.set('bio_cooked', cook(model.get('bio_raw')));
|
||||
selectDefaultTheme(this.get('selectedTheme'));
|
||||
this.set('saved', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
notReady: Em.computed.not('ready'),
|
||||
adminCustomizeCssHtml: Ember.inject.controller(),
|
||||
|
||||
ready: function() {
|
||||
try {
|
||||
const parsed = JSON.parse(this.get('customizationFile'));
|
||||
return !!parsed["site_customization"];
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}.property('customizationFile'),
|
||||
|
||||
actions: {
|
||||
createCustomization: function() {
|
||||
const object = JSON.parse(this.get('customizationFile')).site_customization;
|
||||
|
||||
// Slight fixup before creating object
|
||||
object.enabled = false;
|
||||
delete object.id;
|
||||
delete object.key;
|
||||
|
||||
const controller = this.get('adminCustomizeCssHtml');
|
||||
controller.send('newCustomization', object);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,4 +1,45 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { currentThemeKey } from 'discourse/lib/theme-selector';
|
||||
|
||||
export function refreshCSS(node, hash, newHref, options) {
|
||||
|
||||
let $orig = $(node);
|
||||
|
||||
if ($orig.data('reloading')) {
|
||||
|
||||
if (options && options.force) {
|
||||
clearTimeout($orig.data('timeout'));
|
||||
$orig.data("copy").remove();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$orig.data('orig')) {
|
||||
$orig.data('orig', node.href);
|
||||
}
|
||||
|
||||
$orig.data('reloading', true);
|
||||
|
||||
const orig = $(node).data('orig');
|
||||
|
||||
let reloaded = $orig.clone(true);
|
||||
if (hash) {
|
||||
reloaded[0].href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + hash;
|
||||
} else {
|
||||
reloaded[0].href = newHref;
|
||||
}
|
||||
|
||||
$orig.after(reloaded);
|
||||
|
||||
let timeout = setTimeout(()=>{
|
||||
$orig.remove();
|
||||
reloaded.data('reloading', false);
|
||||
}, 2000);
|
||||
|
||||
$orig.data("timeout", timeout);
|
||||
$orig.data("copy", reloaded);
|
||||
}
|
||||
|
||||
// Use the message bus for live reloading of components for faster development.
|
||||
export default {
|
||||
@ -47,18 +88,17 @@ export default {
|
||||
// Refresh if necessary
|
||||
document.location.reload(true);
|
||||
} else {
|
||||
let themeKey = currentThemeKey();
|
||||
|
||||
$('link').each(function() {
|
||||
// TODO: stop bundling css in DEV please
|
||||
if (true || (this.href.match(me.name) && me.hash)) {
|
||||
if (!$(this).data('orig')) {
|
||||
$(this).data('orig', this.href);
|
||||
if (me.hasOwnProperty('theme_key') && me.new_href) {
|
||||
let target = $(this).data('target');
|
||||
if (me.theme_key === themeKey && target === me.target) {
|
||||
refreshCSS(this, null, me.new_href);
|
||||
}
|
||||
const orig = $(this).data('orig');
|
||||
if (!me.hash) {
|
||||
window.__uniq = window.__uniq || 1;
|
||||
me.hash = window.__uniq++;
|
||||
}
|
||||
this.href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + me.hash;
|
||||
}
|
||||
else if (this.href.match(me.name) && (me.hash || me.new_href)) {
|
||||
refreshCSS(this, me.hash, me.new_href);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -196,19 +196,19 @@ export default {
|
||||
},
|
||||
|
||||
setTrackingToMuted(event) {
|
||||
this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 0, event});
|
||||
this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 0, event});
|
||||
},
|
||||
|
||||
setTrackingToRegular(event) {
|
||||
this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 1, event});
|
||||
this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 1, event});
|
||||
},
|
||||
|
||||
setTrackingToTracking(event) {
|
||||
this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 2, event});
|
||||
this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 2, event});
|
||||
},
|
||||
|
||||
setTrackingToWatching(event) {
|
||||
this.appEvents.trigger('topic-notifications-button:keyboard-trigger', {type: 'notification', id: 3, event});
|
||||
this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: 3, event});
|
||||
},
|
||||
|
||||
sendToTopicListItemView(action) {
|
||||
|
||||
@ -2,47 +2,47 @@ import loadScript from 'discourse/lib/load-script';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default function($elem) {
|
||||
$("a.lightbox", $elem).each(function(i, e) {
|
||||
loadScript("/javascripts/jquery.magnific-popup-min.js").then(function() {
|
||||
const $e = $(e);
|
||||
// do not lightbox spoiled images
|
||||
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
|
||||
if (!$elem) { return; }
|
||||
loadScript("/javascripts/jquery.magnific-popup.min.js").then(function() {
|
||||
const spoilers = $elem.find('.spoiler a.lightbox, .spoiled a.lightbox');
|
||||
$elem.find('a.lightbox').not(spoilers).magnificPopup({
|
||||
type: "image",
|
||||
closeOnContentClick: false,
|
||||
removalDelay: 300,
|
||||
mainClass: "mfp-zoom-in",
|
||||
|
||||
$e.magnificPopup({
|
||||
type: "image",
|
||||
closeOnContentClick: false,
|
||||
removalDelay: 300,
|
||||
mainClass: "mfp-zoom-in",
|
||||
gallery: {
|
||||
enabled: true
|
||||
},
|
||||
|
||||
callbacks: {
|
||||
open() {
|
||||
const wrap = this.wrap,
|
||||
img = this.currItem.img,
|
||||
maxHeight = img.css("max-height");
|
||||
callbacks: {
|
||||
open() {
|
||||
const wrap = this.wrap,
|
||||
img = this.currItem.img,
|
||||
maxHeight = img.css("max-height");
|
||||
|
||||
wrap.on("click.pinhandler", "img", function() {
|
||||
wrap.toggleClass("mfp-force-scrollbars");
|
||||
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
|
||||
});
|
||||
},
|
||||
beforeClose() {
|
||||
this.wrap.off("click.pinhandler");
|
||||
this.wrap.removeClass("mfp-force-scrollbars");
|
||||
}
|
||||
wrap.on("click.pinhandler", "img", function() {
|
||||
wrap.toggleClass("mfp-force-scrollbars");
|
||||
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
|
||||
});
|
||||
},
|
||||
|
||||
image: {
|
||||
titleSrc(item) {
|
||||
const href = item.el.data("download-href") || item.src;
|
||||
let src = [escapeExpression(item.el.attr("title")), $("span.informations", item.el).text().replace('x', '×')];
|
||||
if (!Discourse.SiteSettings.prevent_anons_from_downloading_files || Discourse.User.current()) {
|
||||
src.push('<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>');
|
||||
}
|
||||
return src.join(' · ');
|
||||
}
|
||||
beforeClose() {
|
||||
this.wrap.off("click.pinhandler");
|
||||
this.wrap.removeClass("mfp-force-scrollbars");
|
||||
}
|
||||
},
|
||||
|
||||
image: {
|
||||
titleSrc(item) {
|
||||
const href = item.el.data("download-href") || item.src;
|
||||
let src = [escapeExpression(item.el.attr("title")), $("span.informations", item.el).text().replace('x', '×')];
|
||||
if (!Discourse.SiteSettings.prevent_anons_from_downloading_files || Discourse.User.current()) {
|
||||
src.push('<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>');
|
||||
}
|
||||
return src.join(' · ');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ function loadWithTag(path, cb) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadCSS(url) {
|
||||
return loadScript(url, { css: true });
|
||||
}
|
||||
|
||||
export default function loadScript(url, opts) {
|
||||
|
||||
// TODO: Remove this once plugins have been updated not to use it:
|
||||
@ -47,8 +51,11 @@ export default function loadScript(url, opts) {
|
||||
delete _loading[url];
|
||||
});
|
||||
|
||||
const cb = function() {
|
||||
const cb = function(data) {
|
||||
_loaded[url] = true;
|
||||
if (opts && opts.css) {
|
||||
$("head").append("<style>" + data + "</style>");
|
||||
}
|
||||
done();
|
||||
resolve();
|
||||
};
|
||||
@ -56,7 +63,8 @@ export default function loadScript(url, opts) {
|
||||
var cdnUrl = url;
|
||||
|
||||
// Scripts should always load from CDN
|
||||
if (Discourse.CDN && url[0] === "/" && url[1] !== "/") {
|
||||
// CSS is type text, to accept it from a CDN we would need to handle CORS
|
||||
if (!opts.css && Discourse.CDN && url[0] === "/" && url[1] !== "/") {
|
||||
cdnUrl = Discourse.CDN.replace(/\/$/,"") + url;
|
||||
}
|
||||
|
||||
@ -66,7 +74,7 @@ export default function loadScript(url, opts) {
|
||||
if (opts.scriptTag) {
|
||||
loadWithTag(cdnUrl, cb);
|
||||
} else {
|
||||
ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb);
|
||||
ajax({url: cdnUrl, dataType: opts.css ? "text": "script", cache: true}).then(cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -33,10 +33,10 @@ export default function offsetCalculator(y) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (inter > ideal) {
|
||||
const bottom = $('#topic-bottom').offset().top;
|
||||
const switchPos = bottom - rawWinHeight;
|
||||
|
||||
if (scrollTop > switchPos) {
|
||||
const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0);
|
||||
return ((1 - p) * ideal) + (p * inter);
|
||||
|
||||
@ -43,7 +43,7 @@ export default function(topic, params){
|
||||
buffer = "<div class='discourse-tags'>";
|
||||
if (tags) {
|
||||
for(let i=0; i<tags.length; i++){
|
||||
buffer += renderTag(tags[i]);
|
||||
buffer += renderTag(tags[i]) + ' ';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
65
app/assets/javascripts/discourse/lib/theme-selector.js.es6
Normal file
65
app/assets/javascripts/discourse/lib/theme-selector.js.es6
Normal file
@ -0,0 +1,65 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { refreshCSS } from 'discourse/initializers/live-development';
|
||||
const keySelector = 'meta[name=discourse_theme_key]';
|
||||
|
||||
export function currentThemeKey() {
|
||||
let themeKey = null;
|
||||
let elem = _.first($(keySelector));
|
||||
if (elem) {
|
||||
themeKey = elem.content;
|
||||
if (_.isEmpty(themeKey)) {
|
||||
themeKey = null;
|
||||
}
|
||||
}
|
||||
return themeKey;
|
||||
}
|
||||
|
||||
export function selectDefaultTheme(key) {
|
||||
if (key) {
|
||||
$.cookie('theme_key', key, {path: '/', expires: 9999});
|
||||
} else {
|
||||
$.cookie('theme_key', null, {path: '/', expires: 1});
|
||||
}
|
||||
}
|
||||
|
||||
export function previewTheme(key) {
|
||||
if (currentThemeKey() !== key) {
|
||||
|
||||
Discourse.set("assetVersion", "forceRefresh");
|
||||
|
||||
ajax(`/themes/assets/${key ? key : 'default'}`).then(results => {
|
||||
let elem = _.first($(keySelector));
|
||||
if (elem) {
|
||||
elem.content = key;
|
||||
}
|
||||
|
||||
results.themes.forEach(theme => {
|
||||
let node = $(`link[rel=stylesheet][data-target=${theme.target}]`)[0];
|
||||
if (node) {
|
||||
refreshCSS(node, null, theme.url, {force: true});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function listThemes(site) {
|
||||
let themes = site.get('user_themes');
|
||||
|
||||
if (!themes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let hasDefault = !!themes.findBy('default', true);
|
||||
|
||||
let results = [];
|
||||
if (!hasDefault) {
|
||||
results.push({name: I18n.t('themes.default_description'), id: null});
|
||||
}
|
||||
|
||||
themes.forEach(t=>{
|
||||
results.push({name: t.name, id: t.theme_key});
|
||||
});
|
||||
|
||||
return results.length === 0 ? null : results;
|
||||
}
|
||||
@ -72,21 +72,10 @@ class RouteNode {
|
||||
if (this.name === 'root') {
|
||||
children.forEach(c => c.mapRoutes(router));
|
||||
} else {
|
||||
|
||||
const builder = (children.length === 0) ? undefined : function() {
|
||||
children.forEach(c => c.mapRoutes(this));
|
||||
};
|
||||
router.route(this.name, this.opts, builder);
|
||||
|
||||
// We can have multiple paths to the same route
|
||||
const paths = Object.keys(this.paths);
|
||||
if (paths.length > 1) {
|
||||
paths.filter(p => p !== this.opts.path).forEach(path => {
|
||||
const newOpts = jQuery.extend({}, this.opts, { path });
|
||||
console.log(`warning: we can't have duplicate route names anymore`, newOpts);
|
||||
// router.route(this.name, newOpts, builder);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +73,11 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed('categoryId')
|
||||
category(categoryId) {
|
||||
return categoryId ? this.site.categories.findBy('id', categoryId) : null;
|
||||
},
|
||||
|
||||
creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
|
||||
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
|
||||
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
|
||||
@ -279,13 +284,14 @@ const Composer = RestModel.extend({
|
||||
|
||||
@property minimumTitleLength
|
||||
**/
|
||||
minimumTitleLength: function() {
|
||||
if (this.get('privateMessage')) {
|
||||
@computed('privateMessage')
|
||||
minimumTitleLength(privateMessage) {
|
||||
if (privateMessage) {
|
||||
return this.siteSettings.min_private_message_title_length;
|
||||
} else {
|
||||
return this.siteSettings.min_topic_title_length;
|
||||
}
|
||||
}.property('privateMessage'),
|
||||
},
|
||||
|
||||
@computed('minimumPostLength', 'replyLength', 'canEditTopicFeaturedLink')
|
||||
missingReplyCharacters(minimumPostLength, replyLength, canEditTopicFeaturedLink) {
|
||||
@ -299,16 +305,19 @@ const Composer = RestModel.extend({
|
||||
|
||||
@property minimumPostLength
|
||||
**/
|
||||
minimumPostLength: function() {
|
||||
if( this.get('privateMessage') ) {
|
||||
@computed('privateMessage', 'topicFirstPost', 'topic.pm_with_non_human_user')
|
||||
minimumPostLength(privateMessage, topicFirstPost, pmWithNonHumanUser) {
|
||||
if (pmWithNonHumanUser) {
|
||||
return 1;
|
||||
} else if (privateMessage) {
|
||||
return this.siteSettings.min_private_message_post_length;
|
||||
} else if (this.get('topicFirstPost')) {
|
||||
} else if (topicFirstPost) {
|
||||
// first post (topic body)
|
||||
return this.siteSettings.min_first_post_length;
|
||||
} else {
|
||||
return this.siteSettings.min_post_length;
|
||||
}
|
||||
}.property('privateMessage', 'topicFirstPost'),
|
||||
},
|
||||
|
||||
/**
|
||||
Computes the length of the title minus non-significant whitespaces
|
||||
|
||||
@ -139,7 +139,8 @@ const Group = RestModel.extend({
|
||||
bio_raw: this.get('bio_raw'),
|
||||
public: this.get('public'),
|
||||
allow_membership_requests: this.get('allow_membership_requests'),
|
||||
full_name: this.get('full_name')
|
||||
full_name: this.get('full_name'),
|
||||
default_notification_level: this.get('default_notification_level')
|
||||
};
|
||||
},
|
||||
|
||||
@ -191,10 +192,10 @@ const Group = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
setNotification(notification_level) {
|
||||
setNotification(notification_level, userId) {
|
||||
this.set("group_user.notification_level", notification_level);
|
||||
return ajax(`/groups/${this.get("name")}/notifications`, {
|
||||
data: { notification_level },
|
||||
data: { notification_level, user_id: userId },
|
||||
type: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
@ -56,9 +56,13 @@ export default Ember.Object.extend({
|
||||
},
|
||||
|
||||
findAll(type, findArgs) {
|
||||
const self = this;
|
||||
return this.adapterFor(type).findAll(this, type, findArgs).then(function(result) {
|
||||
return self._resultSet(type, result);
|
||||
const adapter = this.adapterFor(type);
|
||||
return adapter.findAll(this, type, findArgs).then((result) => {
|
||||
let results = this._resultSet(type, result);
|
||||
if (adapter.afterFindAll) {
|
||||
results = adapter.afterFindAll(results);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: { q: {}, expanded: false, context_id: {}, context: {}, skip_context: {} },
|
||||
|
||||
titleToken() {
|
||||
return I18n.t('search.results_page');
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const cached = getTransient('lastSearch');
|
||||
var args = { q: params.q };
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { currentThemeKey } from 'discourse/lib/theme-selector';
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
model() {
|
||||
@ -11,7 +12,8 @@ export default RestrictedUserRoute.extend({
|
||||
controller.reset();
|
||||
controller.setProperties({
|
||||
model: user,
|
||||
newNameInput: user.get('name')
|
||||
newNameInput: user.get('name'),
|
||||
selectedTheme: $.cookie('theme_key') || currentThemeKey()
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||
titleToken() {
|
||||
const model = this.modelFor('topic');
|
||||
if (model) {
|
||||
const result = model.get('title'),
|
||||
const result = model.get('unicode_title') ? model.get('unicode_title') : model.get('title'),
|
||||
cat = model.get('category');
|
||||
|
||||
// Only display uncategorized in the title tag if it was renamed
|
||||
@ -54,7 +54,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||
const model = this.modelFor('topic');
|
||||
model.set('topic_status_update', Ember.Object.create(model.get('topic_status_update')));
|
||||
showModal('edit-topic-status-update', { model });
|
||||
this.controllerFor('modal').set('modalClass', 'topic-close-modal');
|
||||
this.controllerFor('modal').set('modalClass', 'edit-topic-status-update-modal');
|
||||
},
|
||||
|
||||
showChangeTimestamp() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user