Merge master

This commit is contained in:
Neil Lalonde 2017-05-31 16:16:17 -04:00
commit 44a0ff7688
1578 changed files with 84292 additions and 37595 deletions

4
.gitignore vendored
View File

@ -10,6 +10,8 @@ dump.rdb
bin/*
data/
.sass-cache/*
public/csv/*
public/plugins/*
@ -40,6 +42,7 @@ config/discourse.conf
/tmp
/logfile
log/
bootsnap-load-path-cache
# Ignore plugins except for the bundled ones.
/plugins/*
@ -48,6 +51,7 @@ log/
!/plugins/poll/
!/plugins/discourse-details/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated

View File

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

View File

@ -1,8 +0,0 @@
{
"maxReviewers": 2,
"message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.",
"requiredOrgs": ["discourse"],
"skipCollaboratorPR": false,
"delayed": false,
"delayedUntil": "1d"
}

14
.rubocop.yml Normal file
View File

@ -0,0 +1,14 @@
AllCops:
TargetRubyVersion: 2.3
Metrics/LineLength:
Max: 120
Metrics/MethodLength:
Enabled: false
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: False

View File

@ -1 +1 @@
2.3.0
2.4.1

View File

@ -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,12 +21,13 @@ 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.3.1
- 2.4.1
- 2.3.3
services:
- redis-server
@ -42,6 +45,7 @@ before_install:
- git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert
- git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
- git clone --depth=1 https://github.com/discourse/discourse-slack-official.git plugins/discourse-slack-official
- npm i -g eslint babel-eslint
- eslint app/assets/javascripts
- eslint --ext .es6 app/assets/javascripts
@ -56,4 +60,5 @@ 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"

View File

@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
lang_map = el_GR: el, es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml
@ -32,6 +32,18 @@ source_file = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.en.y
source_lang = en
type = YML
[discourse-org.narrativeclientenyml]
file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.narrativeserverenyml]
file_filter = plugins/discourse-narrative-bot/config/locales/server.<lang>.yml
source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.403html]
file_filter = public/403.<lang>.html
source_file = public/403.html

37
Gemfile
View File

@ -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
@ -58,18 +61,16 @@ gem 'fast_xs'
gem 'fast_xor'
# while we sort out https://github.com/sdsykes/fastimage/pull/46
gem 'discourse_fastimage', '2.0.3', require: 'fastimage'
gem 'fastimage', '2.1.0'
gem 'aws-sdk', require: false
gem 'excon', require: false
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'
@ -92,20 +93,16 @@ gem 'pry-rails', require: false
gem 'r2', '~> 0.2.5', require: false
gem 'rake'
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
gem 'execjs', require: false
gem 'mini_racer'
gem 'thin', require: false
gem 'highline', require: false
gem 'rack-protection' # security
@ -118,15 +115,18 @@ group :assets do
end
group :test do
gem 'webmock', require: false
gem 'fakeweb', '~> 1.3.0', require: false
gem 'minitest', require: false
gem 'timecop'
# TODO: Remove once we upgrade to Rails 5.
gem 'test_after_commit'
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,10 +184,11 @@ gem 'rmmseg-cpp', require: false
gem 'logster'
# perftools only works on 1.9 atm
group :profile do
# travis refuses to install this, instead of fuffing, just avoid it for now
#
# if you need to profile, uncomment out this line
# gem 'rack-perftools_profiler', require: 'rack/perftools_profiler', platform: :mri_19
gem 'sassc', require: false
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
gem 'sqlite3', '~> 1.3.13'
end

View File

@ -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,22 +62,30 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.2.2)
bootsnap (0.3.0)
msgpack (~> 1.0)
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)
daemons (1.2.4)
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.5)
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)
@ -94,10 +103,9 @@ GEM
railties (>= 3.1)
ember-source (2.10.0)
erubis (2.7.0)
eventmachine (1.2.0.1)
excon (0.53.0)
excon (0.55.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)
@ -107,15 +115,17 @@ GEM
rake
rake-compiler
fast_xs (0.8.0)
ffi (1.9.10)
fastimage (2.1.0)
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.4)
hashie (3.5.5)
highline (1.7.8)
hiredis (0.6.1)
@ -123,63 +133,59 @@ 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.3)
jwt (1.5.6)
kgio (2.10.0)
libv8 (5.3.332.38.3)
listen (0.7.3)
logster (1.2.5)
kgio (2.11.0)
libv8 (5.3.332.38.5)
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)
lru_redux (1.1.0)
mail (2.6.4)
mail (2.6.5)
mime-types (>= 1.16, < 4)
memory_profiler (0.9.7)
message_bus (2.0.2)
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.7)
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 (0.7.6)
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.2)
mini_portile2 (~> 2.1.0)
nokogumbo (1.4.7)
nokogumbo (1.4.10)
nokogiri
oauth (0.4.7)
oauth (0.5.1)
oauth2 (1.3.1)
faraday (>= 0.8, < 0.12)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (2.17.5)
oj (3.0.5)
omniauth (1.6.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
@ -205,23 +211,22 @@ GEM
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.7.7)
onebox (1.8.8)
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)
@ -230,10 +235,11 @@ 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)
rack-mini-profiler (0.10.1)
rack (1.6.8)
rack-mini-profiler (0.10.4)
rack (>= 1.2.0)
rack-openid (1.3.1)
rack (>= 1.1.0)
@ -242,34 +248,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)
@ -279,7 +285,7 @@ GEM
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
redis (3.3.1)
redis (3.3.3)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
rest-client (1.8.0)
@ -317,17 +323,17 @@ GEM
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
sanitize (4.0.1)
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)
@ -342,8 +348,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)
@ -354,34 +358,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)
thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (0.19.1)
thread_safe (0.3.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
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,11 +400,12 @@ DEPENDENCIES
barber
better_errors
binding_of_caller
bootsnap
bullet
byebug
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)
@ -410,6 +417,7 @@ DEPENDENCIES
fast_blank
fast_xor
fast_xs
fastimage (= 2.1.0)
flamegraph
foreman
gc_tracer
@ -417,8 +425,7 @@ DEPENDENCIES
hiredis
htmlentities
http_accept_language (~> 2.0.5)
image_optim (= 0.20.2)
listen (= 0.7.3)
listen
logster
lru_redux
mail
@ -467,21 +474,21 @@ DEPENDENCIES
rtlit
ruby-readability
sanitize
sass
sass-rails
sassc
seed-fu (~> 2.3.5)
shoulda
sidekiq
sidekiq-statistic
simple-rss
sinatra
spork-rails
stackprof
thin
test_after_commit
thor
timecop
uglifier
unf
unicorn
webmock
BUNDLED WITH
1.13.7
1.14.6

View File

@ -1,24 +1,24 @@
<a href="http://www.discourse.org/">![Logo](images/discourse.png)</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> &nbsp;
<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.

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.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

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

View File

@ -1,12 +1,21 @@
import loadScript from 'discourse/lib/load-script';
import { observes } from 'ember-addons/ember-computed-decorators';
const LOAD_ASYNC = !Ember.Test;
export default Ember.Component.extend({
mode: 'css',
classNames: ['ace-wrapper'],
_editor: null,
_skipContentChangeEvent: null,
@observes('editorId')
editorIdChanged() {
if (this.get('autofocus')) {
this.send('focus');
}
},
@observes('content')
contentChanged() {
if (this._editor && !this._skipContentChangeEvent) {
@ -14,6 +23,13 @@ export default Ember.Component.extend({
}
},
@observes('mode')
modeChanged() {
if (LOAD_ASYNC && this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.get('mode'));
}
},
_destroyEditor: function() {
if (this._editor) {
this._editor.destroy();
@ -23,6 +39,9 @@ export default Ember.Component.extend({
// xxx: don't run during qunit tests
this.appEvents.off('ace:resize', this, this.resize);
}
$(window).off('ace:resize');
}.on('willDestroyElement'),
resize() {
@ -39,23 +58,47 @@ export default Ember.Component.extend({
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
const editor = loadedAce.edit(this.$('.ace')[0]);
editor.setTheme("ace/theme/chrome");
if (LOAD_ASYNC) {
editor.setTheme("ace/theme/chrome");
}
editor.setShowPrintMargin(false);
editor.getSession().setMode("ace/mode/" + this.get('mode'));
editor.setOptions({fontSize: "14px"});
if (LOAD_ASYNC) {
editor.getSession().setMode("ace/mode/" + this.get('mode'));
}
editor.on('change', () => {
this._skipContentChangeEvent = true;
this.set('content', editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10,10);
this.$().data('editor', editor);
this._editor = editor;
$(window).off('ace:resize').on('ace:resize', ()=>{
this.appEvents.trigger('ace:resize');
});
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on('ace:resize', self, self.resize);
this.appEvents.on('ace:resize', ()=>this.resize());
}
if (this.get("autofocus")) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
}
});

View File

@ -0,0 +1,33 @@
import { iconHTML } from 'discourse-common/helpers/fa-icon';
import { bufferedRender } from 'discourse-common/lib/buffered-render';
export default Ember.Component.extend(bufferedRender({
tagName: 'th',
classNames: ['sortable'],
rerenderTriggers: ['order', 'ascending'],
buildBuffer(buffer) {
const icon = this.get('icon');
if (icon) {
buffer.push(iconHTML(icon));
}
buffer.push(I18n.t(this.get('i18nKey')));
if (this.get('field') === this.get('order')) {
buffer.push(iconHTML(this.get('ascending') ? 'chevron-up' : 'chevron-down'));
}
},
click() {
const currentOrder = this.get('order');
const field = this.get('field');
if (currentOrder === field) {
this.set('ascending', this.get('ascending') ? null : true);
} else {
this.setProperties({ order: field, ascending: null });
}
}
}));

View File

@ -0,0 +1,11 @@
export default Ember.Component.extend({
didInsertElement() {
this._super();
$('body').addClass('admin-interface');
},
willDestroyElement() {
this._super();
$('body').removeClass('admin-interface');
}
});

View File

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

View File

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

View File

@ -1,14 +0,0 @@
export default Ember.Component.extend({
willInsertElement() {
this._super();
if (this.session.get("disableCustomCSS")) {
$("link.custom-css").attr("rel", "");
this.session.set("disableCustomCSS", false);
}
},
willDestroyElement() {
this._super();
$("link.custom-css").attr("rel", "stylesheet");
}
});

View File

@ -30,7 +30,7 @@ export default Ember.Component.extend(bufferedProperty('host'), {
save() {
if (this.get('cantSave')) { return; }
const props = this.get('buffered').getProperties('host', 'path_whitelist');
const props = this.get('buffered').getProperties('host', 'path_whitelist', 'class_name');
props.category_id = this.get('categoryId');
const host = this.get('host');

View File

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

View File

@ -1,4 +1,3 @@
import DiscourseURL from 'discourse/lib/url';
import { ajax } from 'discourse/lib/ajax';
export default Ember.Controller.extend({
@ -39,7 +38,11 @@ export default Ember.Controller.extend({
},
download(backup) {
DiscourseURL.redirectTo(backup.get('link'));
let link = backup.get('filename');
ajax("/admin/backups/" + link, { type: "PUT" })
.then(() => {
bootbox.alert(I18n.t("admin.backups.operations.download.alert"));
});
}
},
@ -48,7 +51,7 @@ export default Ember.Controller.extend({
ajax("/admin/backups/readonly", {
type: "PUT",
data: { enable: enable }
}).then(function() {
}).then(() => {
site.set("isReadOnly", enable);
});
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,159 @@
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');
Em.run.next(()=>{
this.appEvents.trigger('ace:resize');
});
}
}
});

View File

@ -0,0 +1,199 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import { url } from 'discourse/lib/computed';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import showModal from 'discourse/lib/show-modal';
const THEME_UPLOAD_VAR = 2;
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);
});
},
addUploadModal() {
showModal('admin-add-upload', {admin: true, name: ''});
},
addUpload(info) {
let model = this.get("model");
model.setField('common', info.name, '', info.upload_id, THEME_UPLOAD_VAR);
model.saveChanges('theme_fields').catch(e => popupAjaxError(e));
},
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);
},
removeUpload(upload) {
return bootbox.confirm(
I18n.t("admin.customize.theme.delete_upload_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"), result => {
if (result) {
this.get("model").removeField(upload);
}
});
},
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');
});
}
});
},
}
});

View File

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

View File

@ -8,10 +8,6 @@ export default Ember.Controller.extend({
showSendEmailForm: Em.computed.notEmpty('model.html_content'),
htmlEmpty: Em.computed.empty('model.html_content'),
iframeSrc: function() {
return ('data:text/html;charset=utf-8,' + encodeURI(this.get('model.html_content')));
}.property('model.html_content'),
actions: {
refresh() {
const model = this.get('model');

View File

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

View File

@ -0,0 +1,4 @@
export default Ember.Controller.extend({
adminGroupsBulk: Ember.inject.controller(),
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
});

View File

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

View File

@ -0,0 +1,11 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
adminGroupsType: Ember.inject.controller(),
sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"),
@computed("sortedGroups")
messageKey(sortedGroups) {
return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`;
}
});

View File

@ -5,9 +5,20 @@ import StaffActionLog from 'admin/models/staff-action-log';
export default Ember.Controller.extend({
loading: false,
filters: null,
userHistoryActions: [],
filtersExists: Ember.computed.gt('filterCount', 0),
filterActionIdChanged: function(){
const filterActionId = this.get('filterActionId');
if (filterActionId) {
this._changeFilters({
action_name: this.get('userHistoryActions').findBy("id", parseInt(filterActionId,10)).name_raw,
action_id: filterActionId
});
}
}.observes('filterActionId'),
actionFilter: function() {
var name = this.get('filters.action_name');
if (name) {
@ -20,7 +31,6 @@ export default Ember.Controller.extend({
showInstructions: Ember.computed.gt('model.length', 0),
refresh: function() {
var self = this;
this.set('loading', true);
var filters = this.get('filters'),
@ -37,10 +47,21 @@ export default Ember.Controller.extend({
});
this.set('filterCount', count);
StaffActionLog.findAll(params).then(function(result) {
self.set('model', result);
}).finally(function() {
self.set('loading', false);
StaffActionLog.findAll(params).then((result) => {
this.set('model', result.staff_action_logs);
if (this.get('userHistoryActions').length === 0) {
let actionTypes = result.user_history_actions.map(pair => {
return {
id: pair.id,
name: I18n.t("admin.logs.staff_actions.actions." + pair.name),
name_raw: pair.name
};
});
actionTypes = _.sortBy(actionTypes, row => row.name);
this.set('userHistoryActions', actionTypes);
}
}).finally(()=>{
this.set('loading', false);
});
},
@ -63,6 +84,7 @@ export default Ember.Controller.extend({
changed.action_name = null;
changed.action_id = null;
changed.custom_type = null;
this.set("filterActionId", null);
} else {
changed[key] = null;
}
@ -70,6 +92,7 @@ export default Ember.Controller.extend({
},
clearAllFilters: function() {
this.set("filterActionId", null);
this.resetFilters();
},

View File

@ -1,16 +1,9 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
@computed('model.@each.enabled_setting')
adminRoutes() {
let routes = [];
this.get('model').forEach(p => {
if (this.siteSettings[p.get('enabled_setting')] && p.get('admin_route')) {
routes.push(p.get('admin_route'));
adminRoutes: function() {
return this.get('model').map(p => {
if (p.get('enabled')) {
return p.admin_route;
}
});
return routes;
}
}).compact();
}.property()
});

View File

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

View File

@ -1,7 +1,6 @@
import debounce from 'discourse/lib/debounce';
export default Ember.Controller.extend({
queryParams: ["filter"],
filter: null,
onlyOverridden: false,
filtered: Ember.computed.notEmpty('filter'),

View File

@ -1,8 +1,13 @@
import { ajax } from 'discourse/lib/ajax';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import { userPath } from 'discourse/lib/url';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend(CanCheckEmails, {
editingUsername: false,
editingName: false,
editingTitle: false,
originalPrimaryGroupId: null,
availableGroups: null,
@ -30,6 +35,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
return [];
}.property('model.user_fields.[]'),
@computed('model.username_lower')
preferencesPath(username) {
return userPath(`${username}/preferences`);
},
actions: {
impersonate() { return this.get("model").impersonate(); },
@ -54,23 +64,58 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
destroy() { return this.get('model').destroy(); },
toggleUsernameEdit() {
this.set('userUsernameValue', this.get('model.username'));
this.toggleProperty('editingUsername');
},
saveUsername() {
const oldUsername = this.get('model.username');
this.set('model.username', this.get('userUsernameValue'));
return ajax(`/users/${oldUsername.toLowerCase()}/preferences/username`, {
data: { new_username: this.get('userUsernameValue') },
type: 'PUT'
}).catch(e => {
this.set('model.username', oldUsername);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingUsername'));
},
toggleNameEdit() {
this.set('userNameValue', this.get('model.name'));
this.toggleProperty('editingName');
},
saveName() {
const oldName = this.get('model.name');
this.set('model.name', this.get('userNameValue'));
return ajax(userPath(`${this.get('model.username').toLowerCase()}.json`), {
data: { name: this.get('userNameValue') },
type: 'PUT'
}).catch(e => {
this.set('model.name', oldName);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingName'));
},
toggleTitleEdit() {
this.set('userTitleValue', this.get('model.title'));
this.toggleProperty('editingTitle');
},
saveTitle() {
const self = this;
const prevTitle = this.get('userTitleValue');
return ajax(`/users/${this.get('model.username').toLowerCase()}.json`, {
this.set('model.title', this.get('userTitleValue'));
return ajax(userPath(`${this.get('model.username').toLowerCase()}.json`), {
data: {title: this.get('userTitleValue')},
type: 'PUT'
}).catch(function(e) {
bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body}));
}).finally(function() {
self.set('model.title', self.get('userTitleValue'));
self.toggleProperty('editingTitle');
});
}).catch(e => {
this.set('model.title', prevTitle);
popupAjaxError(e);
}).finally(() => this.toggleProperty('editingTitle'));
},
generateApiKey() {

View File

@ -1,9 +1,14 @@
import debounce from 'discourse/lib/debounce';
import { i18n } from 'discourse/lib/computed';
import AdminUser from 'admin/models/admin-user';
import { observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
query: null,
queryParams: ['order', 'ascending'],
order: null,
ascending: null,
showEmails: false,
refreshing: false,
listFilter: null,
@ -39,14 +44,15 @@ export default Ember.Controller.extend({
this._refreshUsers();
}, 250).observes('listFilter'),
@observes('order', 'ascending')
_refreshUsers: function() {
var self = this;
this.set('refreshing', true);
AdminUser.findAll(this.get('query'), { filter: this.get('listFilter'), show_emails: this.get('showEmails') }).then(function (result) {
self.set('model', result);
}).finally(function() {
self.set('refreshing', false);
AdminUser.findAll(this.get('query'), { filter: this.get('listFilter'), show_emails: this.get('showEmails'), order: this.get('order'), ascending: this.get('ascending') }).then( (result) => {
this.set('model', result);
}).finally( () => {
this.set('refreshing', false);
});
},

View File

@ -0,0 +1,63 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import { ajax } from 'discourse/lib/ajax';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
adminCustomizeThemesShow: Ember.inject.controller(),
onShow() {
this.set('name', null);
this.set('fileSelected', false);
},
enabled: Em.computed.and('nameValid', 'fileSelected'),
disabled: Em.computed.not('enabled'),
@computed('name')
nameValid(name) {
return name && name.match(/^[a-zA-Z0-9-_]+$/);
},
@observes('name')
uploadChanged(){
let file = $('#file-input')[0];
this.set('fileSelected', file && file.files[0]);
},
actions: {
updateName() {
let name = this.get('name');
if (Em.isEmpty(name)) {
name = $('#file-input')[0].files[0].name;
this.set('name', name.split(".")[0]);
}
this.uploadChanged();
},
upload() {
let options = {
type: 'POST'
};
options.processData = false;
options.contentType = false;
options.data = new FormData();
let file = $('#file-input')[0].files[0];
options.data.append('file', file);
ajax('/admin/themes/upload_asset', options).then(result=>{
let upload = {
upload_id: result.upload_id,
name: this.get('name'),
original_filename: file.name
};
this.get('adminCustomizeThemesShow').send('addUpload', upload);
this.send('closeModal');
}).catch(e => {
popupAjaxError(e);
});
}
}
});

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import IncomingEmail from 'admin/models/incoming-email';
import computed from 'ember-addons/ember-computed-decorators';
import { longDate } from 'discourse/lib/formatter';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
@ -12,6 +13,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
load(id) {
return IncomingEmail.find(id).then(result => this.set("model", result));
},
loadFromBounced(id) {
return IncomingEmail.findByBounced(id)
.then(result => this.set("model", result))
.catch(error => {
this.send("closeModal");
popupAjaxError(error);
});
}
});

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details";
export default ChangeSiteCustomizationDetailsController.extend({
onShow() {
this.send("selectPrevious");
}
});

View File

@ -5,6 +5,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import ApiKey from 'admin/models/api-key';
import Group from 'discourse/models/group';
import TL3Requirements from 'admin/models/tl3-requirements';
import { userPath } from 'discourse/lib/url';
const AdminUser = Discourse.User.extend({
@ -70,7 +71,12 @@ const AdminUser = Discourse.User.extend({
groupRemoved(groupId) {
return ajax("/admin/users/" + this.get('id') + "/groups/" + groupId, {
type: 'DELETE'
}).then(() => this.set('groups.[]', this.get('groups').rejectBy("id", groupId)));
}).then(() => {
this.set('groups.[]', this.get('groups').rejectBy("id", groupId));
if (this.get('primary_group_id') === groupId) {
this.set('primary_group_id', null);
}
});
},
revokeApiKey() {
@ -114,11 +120,10 @@ const AdminUser = Discourse.User.extend({
},
revokeAdmin() {
const self = this;
return ajax("/admin/users/" + this.get('id') + "/revoke_admin", {
return ajax(`/admin/users/${this.get('id')}/revoke_admin`, {
type: 'PUT'
}).then(function() {
self.setProperties({
}).then(() => {
this.setProperties({
admin: false,
can_grant_admin: true,
can_revoke_admin: false
@ -127,15 +132,10 @@ const AdminUser = Discourse.User.extend({
},
grantAdmin() {
const self = this;
return ajax("/admin/users/" + this.get('id') + "/grant_admin", {
return ajax(`/admin/users/${this.get('id')}/grant_admin`, {
type: 'PUT'
}).then(function() {
self.setProperties({
admin: true,
can_grant_admin: false,
can_revoke_admin: true
});
}).then(() => {
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
}).catch(popupAjaxError);
},
@ -346,7 +346,7 @@ const AdminUser = Discourse.User.extend({
},
sendActivationEmail() {
return ajax('/users/action/send_activation_email', {
return ajax(userPath('action/send_activation_email'), {
type: 'POST',
data: { username: this.get('username') }
}).then(function() {

View File

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

View File

@ -19,6 +19,11 @@ IncomingEmail.reopenClass({
return ajax(`/admin/email/incoming/${id}.json`);
},
findByBounced(id) {
return ajax(`/admin/email/incoming_from_bounced/${id}.json`);
},
findAll(filter, offset) {
filter = filter || {};
offset = offset || 0;

View File

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

View File

@ -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')
});
@ -57,10 +57,13 @@ StaffActionLog.reopenClass({
},
findAll: function(filters) {
return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then(function(staff_actions) {
return staff_actions.map(function(s) {
return StaffActionLog.create(s);
});
return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then((data) => {
return {
staff_action_logs: data.staff_action_logs.map(function(s) {
return StaffActionLog.create(s);
}),
user_history_actions: data.user_history_actions
};
});
}
});

View File

@ -0,0 +1,156 @@
import RestModel from 'discourse/models/rest';
import { default as computed } from 'ember-addons/ember-computed-decorators';
const THEME_UPLOAD_VAR = 2;
const Theme = RestModel.extend({
@computed('theme_fields')
themeFields(fields) {
if (!fields) {
this.set('theme_fields', []);
return {};
}
let hash = {};
if (fields) {
fields.forEach(field=>{
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
hash[this.getKey(field)] = field;
}
});
}
return hash;
},
@computed('theme_fields', 'theme_fields.@each')
uploads(fields) {
if (!fields) {
return [];
}
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
},
getKey(field){
return field.target + " " + field.name;
},
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 = this.getKey({target,name});
let field = themeFields[key];
return field ? field.error : "";
},
getField(target, name) {
let themeFields = this.get("themeFields");
let key = this.getKey({target, name});
let field = themeFields[key];
return field ? field.value : "";
},
removeField(field) {
this.set("changed", true);
field.upload_id = null;
field.value = null;
return this.saveChanges("theme_fields");
},
setField(target, name, value, upload_id, type_id) {
this.set("changed", true);
let themeFields = this.get("themeFields");
let field = {name, target, value, upload_id, type_id};
// slow path for uploads and so on
if (type_id && type_id > 1) {
let fields = this.get("theme_fields");
let existing = fields.find((f) =>
f.target === target &&
f.name === name &&
f.type_id === type_id);
if (existing) {
existing.value = value;
existing.upload_id = upload_id;
} else {
fields.push(field);
}
return;
}
// fast path
let key = this.getKey({target,name});
let existingField = themeFields[key];
if (!existingField) {
this.theme_fields.push(field);
themeFields[key] = field;
} else {
existingField.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;

View File

@ -85,7 +85,7 @@ export default Discourse.Route.extend({
if (confirmed) {
Discourse.User.currentProp("hideReadOnlyAlert", true);
backup.restore().then(function() {
self.controllerFor("adminBackupsLogs").clear();
self.controllerFor("adminBackupsLogs").get("logs").clear();
self.controllerFor("adminBackups").set("model.isOperationRunning", true);
self.transitionTo("admin.backups.logs");
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
export default Ember.Route.extend({
beforeModel() {
this.transitionTo('adminCustomize.colors');
this.transitionTo('adminCustomizeThemes');
}
});

View File

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

View File

@ -0,0 +1,5 @@
export default Ember.Route.extend({
setupController() {
this.controllerFor("adminCustomizeThemes").set("editingTheme", false);
},
});

View File

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

View File

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

View File

@ -1,2 +1,14 @@
import showModal from 'discourse/lib/show-modal';
import AdminEmailLogs from 'admin/routes/admin-email-logs';
export default AdminEmailLogs.extend({ status: "bounced" });
export default AdminEmailLogs.extend({
status: "bounced",
actions: {
showIncomingEmail(id) {
showModal('admin-incoming-email', { admin: true });
this.controllerFor("modals/admin-incoming-email").loadFromBounced(id);
}
}
});

View File

@ -1,5 +1,5 @@
export default Discourse.Route.extend({
redirect: function() {
redirect() {
this.replaceWith('adminFlags.list', 'active');
}
});

View File

@ -6,11 +6,6 @@ export default Discourse.Route.extend({
this.render('admin/templates/logs/staff-action-logs', {into: 'adminLogs'});
},
setupController: function(controller) {
controller.resetFilters();
controller.refresh();
},
actions: {
showDetailsModal(model) {
showModal('admin-staff-action-log-details', { model, admin: true });
@ -18,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();
}
}
});

View File

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

View File

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

View File

@ -1,6 +1,10 @@
import SiteSetting from 'admin/models/site-setting';
export default Discourse.Route.extend({
queryParams: {
filter: { replace: true }
},
model() {
return SiteSetting.findAll();
},

View File

@ -28,6 +28,14 @@ export default Discourse.Route.extend({
showSuspendModal(model) {
showModal('admin-suspend-user', { model, admin: true });
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
},
viewActionLogs(username) {
const controller = this.controllerFor('adminLogs.staffActionLogs');
this.transitionTo('adminLogs.staffActionLogs').then(() => {
controller.set('filters', Ember.Object.create());
controller._changeFilters({ target_user: username });
});
}
}
});

View File

@ -1,4 +1,4 @@
{{#disable-custom-stylesheets class="container"}}
{{#admin-wrapper class="container"}}
<div class="row">
<div class="full-width">
@ -34,4 +34,4 @@
</div>
</div>
{{/disable-custom-stylesheets}}
{{/admin-wrapper}}

View File

@ -11,11 +11,11 @@
{{number report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
</td>
<td class="value {{report.sevenDayTrend}}" title={{number report.sevenDayCountTitle}}>
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>
{{number report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
</td>
<td class="value {{report.thirtyDayTrend}}" title={{number report.thirtyDayCountTitle}}>
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>
{{number report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
</td>

View File

@ -1,5 +0,0 @@
<li>
<a href="/admin/customize/css_html/{{customization.id}}/css" class="{{if active 'active'}}">
{{customization.description}}
</a>
</li>

View File

@ -2,6 +2,9 @@
<td>
{{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}}
</td>
<td>
{{input value=buffered.class_name placeholder="class" enter="save" class="class-name"}}
</td>
<td>
{{input value=buffered.path_whitelist placeholder="/blog/.*" enter="save" class="path-whitelist"}}
</td>
@ -14,6 +17,7 @@
</td>
{{else}}
<td>{{host.host}}</td>
<td>{{host.class_name}}</td>
<td>{{host.path_whitelist}}</td>
<td>{{category-badge host.category}}</td>
<td>

View File

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

View File

@ -0,0 +1 @@
<p class="about">{{i18n 'admin.customize.colors.about'}}</p>

View File

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

View File

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

View File

@ -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" download 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"}}&nbsp;{{i18n 'admin.customize.head_tag.text'}}
{{/link-to}}
</li>
<li>
{{#link-to 'adminCustomizeCssHtml.show' model.id 'body-tag'}}
{{fa-icon "file-text-o"}}&nbsp;{{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>

View File

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

View File

@ -0,0 +1,78 @@
<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>
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}
<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>

View File

@ -0,0 +1,140 @@
<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>
<h3>{{i18n "admin.customize.theme.uploads"}}</h3>
{{#if model.uploads}}
<ul class='removable-list'>
{{#each model.uploads as |upload|}}
<li>
<span class='col'>${{upload.name}}: <a href={{upload.url}} target='_blank'>{{upload.filename}}</a></span>
<span class='col'>
{{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}}
</span>
</li>
{{/each}}
</ul>
{{else}}
<p>{{i18n "admin.customize.theme.no_uploads"}}</p>
{{/if}}
<p>
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
</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 class='removable-list'>
{{#each model.childThemes as |child|}}
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" 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>

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

View File

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

View File

@ -261,8 +261,8 @@
<thead>
<tr>
<th class="title">{{top_referrers.title}} ({{i18n 'admin.dashboard.reports.last_30_days'}})</th>
<th>{{number top_referrers.ytitles.num_clicks}}</th>
<th>{{number top_referrers.ytitles.num_topics}}</th>
<th>{{top_referrers.ytitles.num_clicks}}</th>
<th>{{top_referrers.ytitles.num_topics}}</th>
</tr>
</thead>
{{#each top_referrers.data as |r|}}

View File

@ -28,7 +28,7 @@
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td><a {{action "showIncomingEmail" l.id}}>{{l.email_type}}</a></td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.logs.none'}}</td></tr>

View File

@ -43,7 +43,7 @@
{{#if htmlEmpty}}
<p>{{i18n 'admin.email.no_result'}}</p>
{{else}}
<iframe src="{{iframeSrc}}" />
<iframe srcdoc={{model.html_content}} />
{{/if}}
{{else}}
<pre>{{{model.text_content}}}</pre>

View File

@ -2,9 +2,10 @@
{{#if embedding.embeddable_hosts}}
<table class='embedding'>
<tr>
<th style='width: 30%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.path_whitelist"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 25%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 15%'>{{i18n "admin.embedding.class_name"}}</th>
<th style='width: 25%'>{{i18n "admin.embedding.path_whitelist"}}</th>
<th style='width: 25%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 10%'>&nbsp;</th>
</tr>
{{#each embedding.embeddable_hosts as |host|}}
@ -43,6 +44,7 @@
{{embedding-setting field="feed_polling_enabled" value=embedding.feed_polling_enabled type="checkbox"}}
{{embedding-setting field="feed_polling_url" value=embedding.feed_polling_url}}
{{embedding-setting field="feed_polling_frequency_mins" value=embedding.feed_polling_frequency_mins}}
{{embedding-setting field="embed_username_key_from_feed" value=embedding.embed_username_key_from_feed}}
</div>

View File

@ -3,7 +3,6 @@
<table class="admin-flags">
<thead>
<tr>
<th class='user'></th>
<th class='excerpt'></th>
<th class='flaggers'>{{i18n 'admin.flags.flagged_by'}}</th>
<th class='flaggers'>{{#if adminOldFlagsView}}{{i18n 'admin.flags.resolved_by'}}{{/if}}</th>
@ -13,30 +12,42 @@
{{#each content as |flaggedPost|}}
<tr class={{flaggedPost.extraClasses}}>
<td class='user'>
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="large"}}{{/link-to}}
{{#if flaggedPost.wasEdited}}<i class="fa fa-pencil" title="{{i18n 'admin.flags.was_edited'}}"></i>{{/if}}
{{/if}}
{{/if}}
{{#if adminActiveFlagsView}}
{{#if flaggedPost.previous_flags_count}}
<span title="{{i18n 'admin.flags.previous_flags_count' count=flaggedPost.previous_flags_count}}" class="badge-notification flagged-posts">{{flaggedPost.previous_flags_count}}</span>
{{/if}}
{{/if}}
</td>
<td class='excerpt'>
<h3>
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
<div class="flex-center-align">
<div class="flagged-post-avatar">
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="large"}}{{/link-to}}
{{#if flaggedPost.wasEdited}}<i class="fa fa-pencil" title="{{i18n 'admin.flags.was_edited'}}"></i>{{/if}}
{{/if}}
{{/if}}
{{#if adminActiveFlagsView}}
{{#if flaggedPost.previous_flags_count}}
<span title="{{i18n 'admin.flags.previous_flags_count' count=flaggedPost.previous_flags_count}}" class="badge-notification flagged-posts">{{flaggedPost.previous_flags_count}}</span>
{{/if}}
{{/if}}
</div>
<div class="topic-excerpt">
<h3>
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
</h3>
{{#unless site.mobileView}}
{{#if flaggedPost.postAuthorFlagged}}
<p>{{{flaggedPost.excerpt}}}</p>
{{/if}}
{{/unless}}
</div>
</div>
{{#if site.mobileView}}
{{#if flaggedPost.postAuthorFlagged}}
<p>{{{flaggedPost.excerpt}}}</p>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
</h3>
{{#if flaggedPost.postAuthorFlagged}}
<p>{{{flaggedPost.excerpt}}}</p>
{{/if}}
</td>
@ -104,7 +115,6 @@
{{#each flaggedPost.conversations as |c|}}
<tr class='message'>
<td></td>
<td colspan="3">
<div>
{{#if c.response}}
@ -130,7 +140,7 @@
{{#unless adminOldFlagsView}}
<tr>
<td colspan="4" class="action">
<td colspan="3" class="action">
{{#if adminActiveFlagsView}}
<button title='{{i18n 'admin.flags.agree_title'}}' class='btn' {{action "showAgreeFlagModal" flaggedPost}}><i class="fa fa-thumbs-o-up"></i>{{i18n 'admin.flags.agree'}}&hellip;</button>
{{#if flaggedPost.postHidden}}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<div class="groups-type-index">
<p>{{i18n messageKey}}</p>
<div>
{{#link-to 'adminGroup' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
{{/link-to}}
</div>
</div>

View File

@ -1,29 +1,31 @@
<div class='row groups'>
<div class='content-list span6'>
<h3>{{i18n 'admin.groups.edit'}}</h3>
<ul>
{{#each sortedGroups as |group|}}
<li>
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
{{#if group.userCountDisplay}}
<span class="count">{{number group.userCountDisplay}}</span>
{{/if}}
{{#if sortedGroups}}
<div class='content-list span6'>
<h3>{{i18n 'admin.groups.edit'}}</h3>
<ul>
{{#each sortedGroups as |group|}}
<li>
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
{{#if group.userCountDisplay}}
<span class="count">{{number group.userCountDisplay}}</span>
{{/if}}
{{/link-to}}
</li>
{{/each}}
</ul>
<div class='controls'>
{{#if isAuto}}
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
{{else}}
{{#link-to 'adminGroup' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
{{/link-to}}
</li>
{{/each}}
</ul>
<div class='controls'>
{{#if isAuto}}
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
{{else}}
{{#link-to 'adminGroup' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n 'admin.groups.new'}}
{{/link-to}}
{{/if}}
{{/if}}
</div>
</div>
</div>
{{/if}}
<div class='content-editor'>
<div class="span13">
{{outlet}}
</div>
</div>

View File

@ -1,41 +1,43 @@
<div class="staff-action-logs-controls">
<a {{action "clearAllFilters"}} class="clear-filters filter {{unless filtersExists 'invisible'}}">
<span class="label">{{i18n 'admin.logs.staff_actions.clear_filters'}}</span>
</a>
{{#if actionFilter}}
<a {{action "clearFilter" "actionFilter"}} class="filter">
<span class="label">{{i18n 'admin.logs.action'}}</span>: {{actionFilter}}
{{fa-icon "times-circle"}}
{{#if filtersExists}}
<div>
<a {{action "clearAllFilters"}} class="clear-filters filter">
<span class="label">{{i18n 'admin.logs.staff_actions.clear_filters'}}</span>
</a>
{{#if actionFilter}}
<a {{action "clearFilter" "actionFilter"}} class="filter">
<span class="label">{{i18n 'admin.logs.action'}}</span>: {{actionFilter}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.acting_user}}
<a {{action "clearFilter" "acting_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.staff_user'}}</span>: {{filters.acting_user}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.target_user}}
<a {{action "clearFilter" "target_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.target_user'}}</span>: {{filters.target_user}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.subject}}
<a {{action "clearFilter" "subject"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.subject'}}</span>: {{filters.subject}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
</div>
{{else}}
{{i18n "admin.logs.staff_actions.filter"}} {{combo-box content=userHistoryActions nameProperty="name" value=filterActionId none="admin.logs.staff_actions.all"}}
{{/if}}
{{#if filters.acting_user}}
<a {{action "clearFilter" "acting_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.staff_user'}}</span>: {{filters.acting_user}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.target_user}}
<a {{action "clearFilter" "target_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.target_user'}}</span>: {{filters.target_user}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.subject}}
<a {{action "clearFilter" "subject"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.subject'}}</span>: {{filters.subject}}
{{fa-icon "times-circle"}}
</a>
{{/if}}
</div>
<div class="pull-right">
{{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
</div>
<br>
<div class="staff-action-logs-instructions {{unless showInstructions 'invisible'}}">
{{i18n 'admin.logs.staff_actions.instructions'}}
<div class="pull-right">
{{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
</div>
</div>
<div class="clearfix"></div>
<div class='table staff-actions'>
<div class="heading-container">

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