| @@ -0,0 +1,113 @@ | ||
| +AllCops: | ||
| + TargetRubyVersion: 2.4 | ||
| + DisabledByDefault: true | ||
| + Exclude: | ||
| + - 'db/schema.rb' | ||
| + - 'bundle/**/*' | ||
| + - 'vendor/**/*' | ||
| + - 'node_modules/**/*' | ||
| + - 'public/**/*' | ||
| + | ||
| +# Prefer &&/|| over and/or. | ||
| +Style/AndOr: | ||
| + Enabled: true | ||
| + | ||
| +# Do not use braces for hash literals when they are the last argument of a | ||
| +# method call. | ||
| +Style/BracesAroundHashParameters: | ||
| + Enabled: true | ||
| + | ||
| +# Align `when` with `case`. | ||
| +Layout/CaseIndentation: | ||
| + Enabled: true | ||
| + | ||
| +# Align comments with method definitions. | ||
| +Layout/CommentIndentation: | ||
| + Enabled: true | ||
| + | ||
| +# No extra empty lines. | ||
| +Layout/EmptyLines: | ||
| + Enabled: true | ||
| + | ||
| +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. | ||
| +Style/HashSyntax: | ||
| + Enabled: true | ||
| + | ||
| +# Two spaces, no tabs (for indentation). | ||
| +Layout/IndentationWidth: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceAfterColon: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceAfterComma: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceAroundEqualsInParameterDefault: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceAroundKeyword: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceAroundOperators: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceBeforeFirstArg: | ||
| + Enabled: true | ||
| + | ||
| +# Defining a method with parameters needs parentheses. | ||
| +Style/MethodDefParentheses: | ||
| + Enabled: true | ||
| + | ||
| +# Use `foo {}` not `foo{}`. | ||
| +Layout/SpaceBeforeBlockBraces: | ||
| + Enabled: true | ||
| + | ||
| +# Use `foo { bar }` not `foo {bar}`. | ||
| +Layout/SpaceInsideBlockBraces: | ||
| + Enabled: true | ||
| + | ||
| +# Use `{ a: 1 }` not `{a:1}`. | ||
| +Layout/SpaceInsideHashLiteralBraces: | ||
| + Enabled: true | ||
| + | ||
| +Layout/SpaceInsideParens: | ||
| + Enabled: true | ||
| + | ||
| +# Detect hard tabs, no hard tabs. | ||
| +Layout/Tab: | ||
| + Enabled: true | ||
| + | ||
| +# Blank lines should not have any spaces. | ||
| +Layout/TrailingBlankLines: | ||
| + Enabled: true | ||
| + | ||
| +# No trailing whitespace. | ||
| +Layout/TrailingWhitespace: | ||
| + Enabled: true | ||
| + | ||
| +Lint/Debugger: | ||
| + Enabled: true | ||
| + | ||
| +Layout/BlockAlignment: | ||
| + Enabled: true | ||
| + | ||
| +# Align `end` with the matching keyword or starting expression except for | ||
| +# assignments, where it should be aligned with the LHS. | ||
| +Layout/EndAlignment: | ||
| + Enabled: true | ||
| + EnforcedStyleAlignWith: variable | ||
| + | ||
| +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. | ||
| +Lint/RequireParentheses: | ||
| + Enabled: true | ||
| + | ||
| +Layout/MultilineMethodCallIndentation: | ||
| + Enabled: true | ||
| + EnforcedStyle: indented | ||
| + | ||
| +Layout/AlignHash: | ||
| + Enabled: true | ||
| + | ||
| +Bundler/OrderedGems: | ||
| + Enabled: false |
| @@ -1 +1 @@ | ||
| -ruby-2.0.0-p195 | ||
| +2.4.1 |
| @@ -1,92 +1,93 @@ | ||
| [main] | ||
| host = https://www.transifex.com | ||
| +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 | ||
| source_file = config/locales/client.en.yml | ||
| source_lang = en | ||
| -trans.es_ES = config/locales/client.es.yml | ||
| -trans.fr_FR = config/locales/client.fr.yml | ||
| -trans.ko_KR = config/locales/client.ko.yml | ||
| -trans.pt_PT = config/locales/client.pt.yml | ||
| type = YML | ||
| [discourse-org.serverenyml] | ||
| file_filter = config/locales/server.<lang>.yml | ||
| source_file = config/locales/server.en.yml | ||
| source_lang = en | ||
| -trans.es_ES = config/locales/server.es.yml | ||
| -trans.fr_FR = config/locales/server.fr.yml | ||
| -trans.ko_KR = config/locales/server.ko.yml | ||
| -trans.pt_PT = config/locales/server.pt.yml | ||
| type = YML | ||
| [discourse-org.pollclientenyml] | ||
| file_filter = plugins/poll/config/locales/client.<lang>.yml | ||
| source_file = plugins/poll/config/locales/client.en.yml | ||
| source_lang = en | ||
| -trans.es_ES = plugins/poll/config/locales/client.es.yml | ||
| -trans.fr_FR = plugins/poll/config/locales/client.fr.yml | ||
| -trans.ko_KR = plugins/poll/config/locales/client.ko.yml | ||
| -trans.pt_PT = plugins/poll/config/locales/client.pt.yml | ||
| type = YML | ||
| [discourse-org.pollserverenyml] | ||
| file_filter = plugins/poll/config/locales/server.<lang>.yml | ||
| source_file = plugins/poll/config/locales/server.en.yml | ||
| source_lang = en | ||
| -trans.es_ES = plugins/poll/config/locales/server.es.yml | ||
| -trans.fr_FR = plugins/poll/config/locales/server.fr.yml | ||
| -trans.ko_KR = plugins/poll/config/locales/server.ko.yml | ||
| -trans.pt_PT = plugins/poll/config/locales/server.pt.yml | ||
| type = YML | ||
| -[discourse-org.imgurserverenyml] | ||
| -file_filter = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.<lang>.yml | ||
| -source_file = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.en.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.discourse-presenceclientenyml] | ||
| +file_filter = plugins/discourse-presence/config/locales/client.<lang>.yml | ||
| +source_file = plugins/discourse-presence/config/locales/client.en.yml | ||
| +source_lang = en | ||
| +type = YML | ||
| + | ||
| +[discourse-org.discourse-presenceserverenyml] | ||
| +file_filter = plugins/discourse-presence/config/locales/server.<lang>.yml | ||
| +source_file = plugins/discourse-presence/config/locales/server.en.yml | ||
| +source_lang = en | ||
| +type = YML | ||
| + | ||
| +[discourse-org.coreplugindetailsclientyml] | ||
| +file_filter = plugins/discourse-details/config/locales/client.<lang>.yml | ||
| +source_file = plugins/discourse-details/config/locales/client.en.yml | ||
| +source_lang = en | ||
| +type = YML | ||
| + | ||
| +[discourse-org.coreplugindetailsserveryml] | ||
| +file_filter = plugins/discourse-details/config/locales/server.<lang>.yml | ||
| +source_file = plugins/discourse-details/config/locales/server.en.yml | ||
| +source_lang = en | ||
| +type = YML | ||
| + | ||
| +[discourse-org.corepluginnginx-performance-reportserveryml] | ||
| +file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml | ||
| +source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml | ||
| source_lang = en | ||
| -trans.es_ES = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.es.yml | ||
| -trans.fr_FR = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.fr.yml | ||
| -trans.ko_KR = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.ko.yml | ||
| -trans.pt_PT = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.pt.yml | ||
| type = YML | ||
| [discourse-org.403html] | ||
| file_filter = public/403.<lang>.html | ||
| source_file = public/403.html | ||
| source_lang = en | ||
| -trans.es_ES = public/403.es.html | ||
| -trans.fr_FR = public/403.fr.html | ||
| -trans.ko_KR = public/403.ko.html | ||
| -trans.pt_PT = public/403.pt.html | ||
| type = HTML | ||
| [discourse-org.422html] | ||
| file_filter = public/422.<lang>.html | ||
| source_file = public/422.html | ||
| source_lang = en | ||
| -trans.es_ES = public/422.es.html | ||
| -trans.fr_FR = public/422.fr.html | ||
| -trans.ko_KR = public/422.ko.html | ||
| -trans.pt_PT = public/422.pt.html | ||
| type = HTML | ||
| [discourse-org.500html] | ||
| file_filter = public/500.<lang>.html | ||
| source_file = public/500.html | ||
| source_lang = en | ||
| -trans.es_ES = public/500.es.html | ||
| -trans.fr_FR = public/500.fr.html | ||
| -trans.ko_KR = public/500.ko.html | ||
| -trans.pt_PT = public/500.pt.html | ||
| type = HTML | ||
| [discourse-org.503html] | ||
| file_filter = public/503.<lang>.html | ||
| source_file = public/503.html | ||
| source_lang = en | ||
| -trans.es_ES = public/503.es.html | ||
| -trans.fr_FR = public/503.fr.html | ||
| -trans.ko_KR = public/503.ko.html | ||
| -trans.pt_PT = public/503.pt.html | ||
| type = HTML |
| @@ -1,22 +1,10 @@ | ||
| # Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew) | ||
| -# ensure that Homebrew's sources are up to date | ||
| -update | ||
| - | ||
| -# add this repo to Homebrew's sources | ||
| -tap homebrew/dupes | ||
| - | ||
| -# install the gcc compiler required for ruby | ||
| -install apple-gcc42 | ||
| - | ||
| # you probably already have git installed; ensure that it is the latest version | ||
| -install git | ||
| +brew 'git' | ||
| # install the PostgreSQL database | ||
| -install postgresql | ||
| +brew 'postgresql' | ||
| # install the Redis datastore | ||
| -install redis | ||
| - | ||
| -# install headless Javascript testing library | ||
| -install phantomjs | ||
| +brew 'redis' |
| @@ -1,129 +1,27 @@ | ||
| # Contributing to Discourse | ||
| -## Before You Start | ||
| +## Important note for Developers | ||
| -Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first. | ||
| +Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first. | ||
| -## Reporting Bugs | ||
| +For more information on | ||
| -1. Always update to the most recent master release; the bug may already be resolved. | ||
| +- how to set up your development environment | ||
| +- first-time project suggestions | ||
| +- code conventions | ||
| +- step-by-step guide for GitHub commits | ||
| -2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem. | ||
| +**please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)** | ||
| -3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org) | ||
| +## Everything Else | ||
| -4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**. | ||
| +There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below. | ||
| -5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section). | ||
| +- [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070) | ||
| +- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986) | ||
| +- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882) | ||
| +- Documentation (TBA) | ||
| -6. When the bug is fixed, we will do our best to update the Discourse topic. | ||
| +For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on. | ||
| -## Requesting New Features | ||
| - | ||
| -1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing. | ||
| - | ||
| -2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit. | ||
| - | ||
| -3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below). | ||
| - | ||
| -## Contributing (Step-by-step) | ||
| - | ||
| -1. Clone the Repo: | ||
| - | ||
| - git clone git://github.com/discourse/discourse.git | ||
| - | ||
| -2. Create a new Branch: | ||
| - | ||
| - cd discourse | ||
| - git checkout -b new_discourse_branch | ||
| - | ||
| - > Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead. | ||
| - | ||
| -3. Code | ||
| - * Adhere to common conventions you see in the existing code | ||
| - * Include tests, and ensure they pass | ||
| - * Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate | ||
| - | ||
| -4. Follow the Coding Conventions | ||
| - * two spaces, no tabs | ||
| - * no trailing whitespaces, blank lines should have no spaces | ||
| - * use spaces around operators, after commas, colons, semicolons, around `{` and before `}` | ||
| - * no space after `(`, `[` or before `]`, `)` | ||
| - * use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }` | ||
| - * prefer `class << self; def method; end` over `def self.method` for class methods | ||
| - * prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks | ||
| - * avoid `return` when not required | ||
| - | ||
| - > However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome. | ||
| - | ||
| -5. Commit | ||
| - | ||
| - For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling. | ||
| - | ||
| - **NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit! | ||
| - | ||
| - | ||
| -6. Update your branch | ||
| - | ||
| - ``` | ||
| - git fetch origin | ||
| - git rebase origin/master | ||
| - ``` | ||
| - | ||
| -7. Fork | ||
| - | ||
| - ``` | ||
| - git remote add mine git@github.com:<your user name>/discourse.git | ||
| - ``` | ||
| - | ||
| -8. Push to your remote | ||
| - | ||
| - ``` | ||
| - git push mine new_discourse_branch | ||
| - ``` | ||
| - | ||
| -9. Issue a Pull Request | ||
| - | ||
| - Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command: | ||
| - | ||
| - ``` | ||
| - git fetch origin | ||
| - git checkout new_discourse_branch | ||
| - git rebase origin/master | ||
| - git rebase -i | ||
| - | ||
| - < the editor opens and allows you to change the commit history > | ||
| - < follow the instructions on the bottom of the editor > | ||
| - | ||
| - git push -f mine new_discourse_branch | ||
| - ``` | ||
| - | ||
| - | ||
| - In order to make a pull request, | ||
| - * Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse) | ||
| - * Click "Pull Request". | ||
| - * Write your branch name in the branch field (this is filled with "master" by default) | ||
| - * Click "Update Commit Range". | ||
| - * Ensure the changesets you introduced are included in the "Commits" tab. | ||
| - * Ensure that the "Files Changed" incorporate all of your changes. | ||
| - * Fill in some details about your potential patch including a meaningful title. | ||
| - * Click "Send pull request". | ||
| - | ||
| - Thanks for that -- we'll get to your pull request ASAP, we love pull requests! | ||
| - | ||
| -10. Responding to Feedback | ||
| - | ||
| - The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own. | ||
| - | ||
| - > Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top). | ||
| - | ||
| -## Translations | ||
| - | ||
| -Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics: | ||
| - | ||
| -* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882) | ||
| -* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970) | ||
| - | ||
| - | ||
| - | ||
| -[m]: http://meta.discourse.org | ||
| +*Thanks for contributing!* |
| @@ -1,65 +0,0 @@ | ||
| -# -*- mode: ruby -*- | ||
| -# vi: set ft=ruby : | ||
| -# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md | ||
| -# | ||
| -Vagrant.configure("2") do |config| | ||
| - config.vm.box= "discourse/discourse-0.9.9.15.box" | ||
| - config.vm.box_url = "https://vagrantcloud.com/discourse/discourse-0.9.9.15.box" | ||
| - | ||
| - # Make this VM reachable on the host network as well, so that other | ||
| - # VM's running other browsers can access our dev server. | ||
| - config.vm.network :private_network, ip: "192.168.10.200" | ||
| - | ||
| - # Make it so that network access from the vagrant guest is able to | ||
| - # use SSH private keys that are present on the host without copying | ||
| - # them into the VM. | ||
| - config.ssh.forward_agent = true | ||
| - | ||
| - config.vm.provider :virtualbox do |v| | ||
| - # This setting gives the VM 1024MB of RAM instead of the default 384. | ||
| - v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max] | ||
| - | ||
| - # Who has a single core cpu these days anyways? | ||
| - cpu_count = 2 | ||
| - | ||
| - # Determine the available cores in host system. | ||
| - # This mostly helps on linux, but it couldn't hurt on MacOSX. | ||
| - if RUBY_PLATFORM =~ /linux/ | ||
| - cpu_count = `nproc`.to_i | ||
| - elsif RUBY_PLATFORM =~ /darwin/ | ||
| - cpu_count = `sysctl -n hw.ncpu`.to_i | ||
| - end | ||
| - | ||
| - # Assign additional cores to the guest OS. | ||
| - v.customize ["modifyvm", :id, "--cpus", cpu_count] | ||
| - v.customize ["modifyvm", :id, "--ioapic", "on"] | ||
| - | ||
| - # This setting makes it so that network access from inside the vagrant guest | ||
| - # is able to resolve DNS using the hosts VPN connection. | ||
| - v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] | ||
| - end | ||
| - | ||
| - config.vm.network :forwarded_port, guest: 3000, host: 4000 | ||
| - config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher | ||
| - | ||
| - nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/ | ||
| - config.vm.synced_folder ".", "/vagrant", id: "vagrant-root", :nfs => nfs_setting | ||
| - | ||
| - config.vm.provision :shell, :inline => "apt-get -qq update && apt-get -qq -y install ruby1.9.3 build-essential && gem install chef --no-rdoc --no-ri --conservative" | ||
| - | ||
| - chef_cookbooks_path = ["chef/cookbooks"] | ||
| - | ||
| - # This run uses the updated chef-solo and does normal configuration | ||
| - config.vm.provision :chef_solo do |chef| | ||
| - chef.binary_env = "GEM_HOME=/opt/chef/embedded/lib/ruby/gems/1.9.1/ GEM_PATH= " | ||
| - chef.binary_path = "/opt/chef/bin/" | ||
| - chef.cookbooks_path = chef_cookbooks_path | ||
| - | ||
| - chef.add_recipe "recipe[apt]" | ||
| - chef.add_recipe "recipe[build-essential]" | ||
| - chef.add_recipe "recipe[vim]" | ||
| - chef.add_recipe "recipe[java]" | ||
| - chef.add_recipe "recipe[imagemagick]" | ||
| - chef.add_recipe "discourse" | ||
| - end | ||
| -end |
| @@ -0,0 +1,9 @@ | ||
| +import RestAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default function buildPluginAdapter(pluginName) { | ||
| + return RestAdapter.extend({ | ||
| + pathFor(store, type, findArgs) { | ||
| + return "/admin/plugins/" + pluginName + this._super(store, type, findArgs); | ||
| + } | ||
| + }); | ||
| +} |
| @@ -0,0 +1,7 @@ | ||
| +import RestAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default RestAdapter.extend({ | ||
| + basePath() { | ||
| + return "/admin/customize/"; | ||
| + } | ||
| +}); |
| @@ -0,0 +1,7 @@ | ||
| +import RestAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default RestAdapter.extend({ | ||
| + pathFor() { | ||
| + return "/admin/customize/embedding"; | ||
| + } | ||
| +}); |
| @@ -0,0 +1,38 @@ | ||
| +import RestAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default RestAdapter.extend({ | ||
| + pathFor(store, type, findArgs) { | ||
| + let args = _.merge({ rest_api: true }, findArgs); | ||
| + delete args.filter; | ||
| + return `/admin/flags/${findArgs.filter}.json?${$.param(args)}`; | ||
| + }, | ||
| + | ||
| + afterFindAll(results, helper) { | ||
| + results.forEach(flag => { | ||
| + let conversations = []; | ||
| + flag.post_actions.forEach(pa => { | ||
| + if (pa.conversation) { | ||
| + let conversation = { | ||
| + permalink: pa.permalink, | ||
| + hasMore: pa.conversation.has_more, | ||
| + response: { | ||
| + excerpt: pa.conversation.response.excerpt, | ||
| + user: helper.lookup('user', pa.conversation.response.user_id) | ||
| + } | ||
| + }; | ||
| + | ||
| + if (pa.conversation.reply) { | ||
| + conversation.reply = { | ||
| + excerpt: pa.conversation.reply.excerpt, | ||
| + user: helper.lookup('user', pa.conversation.reply.user_id) | ||
| + }; | ||
| + } | ||
| + conversations.push(conversation); | ||
| + } | ||
| + }); | ||
| + flag.set('conversations', conversations); | ||
| + }); | ||
| + | ||
| + return results; | ||
| + } | ||
| +}); |
| @@ -0,0 +1,2 @@ | ||
| +import CustomizationBase from 'admin/adapters/customization-base'; | ||
| +export default CustomizationBase; |
| @@ -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 | ||
| +}); |
| @@ -0,0 +1,2 @@ | ||
| +import CustomizationBase from 'admin/adapters/customization-base'; | ||
| +export default CustomizationBase; |
| @@ -0,0 +1,7 @@ | ||
| +import RESTAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default RESTAdapter.extend({ | ||
| + basePath() { | ||
| + return '/admin/api/'; | ||
| + } | ||
| +}); |
| @@ -0,0 +1,7 @@ | ||
| +import RESTAdapter from 'discourse/adapters/rest'; | ||
| + | ||
| +export default RESTAdapter.extend({ | ||
| + basePath() { | ||
| + return '/admin/api/'; | ||
| + } | ||
| +}); |
| @@ -0,0 +1,124 @@ | ||
| +import loadScript from 'discourse/lib/load-script'; | ||
| +import { observes } from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +const LOAD_ASYNC = !Ember.testing; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + mode: 'css', | ||
| + classNames: ['ace-wrapper'], | ||
| + _editor: null, | ||
| + _skipContentChangeEvent: null, | ||
| + disabled: false, | ||
| + | ||
| + @observes('editorId') | ||
| + editorIdChanged() { | ||
| + if (this.get('autofocus')) { | ||
| + this.send('focus'); | ||
| + } | ||
| + }, | ||
| + | ||
| + @observes('content') | ||
| + contentChanged() { | ||
| + if (this._editor && !this._skipContentChangeEvent) { | ||
| + this._editor.getSession().setValue(this.get('content')); | ||
| + } | ||
| + }, | ||
| + | ||
| + @observes('mode') | ||
| + modeChanged() { | ||
| + if (LOAD_ASYNC && this._editor && !this._skipContentChangeEvent) { | ||
| + this._editor.getSession().setMode("ace/mode/" + this.get('mode')); | ||
| + } | ||
| + }, | ||
| + | ||
| + @observes('disabled') | ||
| + disabledStateChanged() { | ||
| + this.changeDisabledState(); | ||
| + }, | ||
| + | ||
| + changeDisabledState() { | ||
| + const editor = this._editor; | ||
| + if (editor) { | ||
| + const disabled = this.get('disabled'); | ||
| + editor.setOptions({ | ||
| + readOnly: disabled, | ||
| + highlightActiveLine: !disabled, | ||
| + highlightGutterLine: !disabled | ||
| + }); | ||
| + editor.container.parentNode.setAttribute("data-disabled", disabled); | ||
| + } | ||
| + }, | ||
| + | ||
| + _destroyEditor: function() { | ||
| + if (this._editor) { | ||
| + this._editor.destroy(); | ||
| + this._editor = null; | ||
| + } | ||
| + if (this.appEvents) { | ||
| + // xxx: don't run during qunit tests | ||
| + this.appEvents.off('ace:resize', this, this.resize); | ||
| + } | ||
| + | ||
| + $(window).off('ace:resize'); | ||
| + | ||
| + }.on('willDestroyElement'), | ||
| + | ||
| + resize() { | ||
| + if (this._editor) { | ||
| + this._editor.resize(); | ||
| + } | ||
| + }, | ||
| + | ||
| + didInsertElement() { | ||
| + this._super(); | ||
| + | ||
| + loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(() => { | ||
| + window.ace.require(['ace/ace'], loadedAce => { | ||
| + if (!this.element || this.isDestroying || this.isDestroyed) { return; } | ||
| + const editor = loadedAce.edit(this.$('.ace')[0]); | ||
| + | ||
| + if (LOAD_ASYNC) { | ||
| + editor.setTheme("ace/theme/chrome"); | ||
| + } | ||
| + editor.setShowPrintMargin(false); | ||
| + 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; | ||
| + this.changeDisabledState(); | ||
| + | ||
| + $(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', ()=>this.resize()); | ||
| + } | ||
| + | ||
| + if (this.get("autofocus")) { | ||
| + this.send("focus"); | ||
| + } | ||
| + }); | ||
| + }); | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + focus() { | ||
| + if (this._editor) { | ||
| + this._editor.focus(); | ||
| + this._editor.navigateFileEnd(); | ||
| + } | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,57 @@ | ||
| +import debounce from 'discourse/lib/debounce'; | ||
| +import { renderSpinner } from 'discourse/helpers/loading-spinner'; | ||
| +import { escapeExpression } from 'discourse/lib/utilities'; | ||
| +import { bufferedRender } from 'discourse-common/lib/buffered-render'; | ||
| + | ||
| +export default Ember.Component.extend(bufferedRender({ | ||
| + classNames: ["admin-backups-logs"], | ||
| + | ||
| + init() { | ||
| + this._super(); | ||
| + this._reset(); | ||
| + }, | ||
| + | ||
| + _reset() { | ||
| + this.setProperties({ formattedLogs: "", index: 0 }); | ||
| + }, | ||
| + | ||
| + _scrollDown() { | ||
| + const $div = this.$()[0]; | ||
| + $div.scrollTop = $div.scrollHeight; | ||
| + }, | ||
| + | ||
| + _updateFormattedLogs: debounce(function() { | ||
| + const logs = this.get("logs"); | ||
| + if (logs.length === 0) { | ||
| + this._reset(); // reset the cached logs whenever the model is reset | ||
| + } else { | ||
| + // do the log formatting only once for HELLish performance | ||
| + let formattedLogs = this.get("formattedLogs"); | ||
| + for (let i = this.get("index"), length = logs.length; i < length; i++) { | ||
| + const date = logs[i].get("timestamp"), | ||
| + message = escapeExpression(logs[i].get("message")); | ||
| + formattedLogs += "[" + date + "] " + message + "\n"; | ||
| + } | ||
| + // update the formatted logs & cache index | ||
| + this.setProperties({ formattedLogs: formattedLogs, index: logs.length }); | ||
| + // force rerender | ||
| + this.rerenderBuffer(); | ||
| + } | ||
| + Ember.run.scheduleOnce('afterRender', this, this._scrollDown); | ||
| + }, 150).observes("logs.[]").on('init'), | ||
| + | ||
| + buildBuffer(buffer) { | ||
| + const formattedLogs = this.get("formattedLogs"); | ||
| + if (formattedLogs && formattedLogs.length > 0) { | ||
| + buffer.push("<pre>"); | ||
| + buffer.push(formattedLogs); | ||
| + buffer.push("</pre>"); | ||
| + } else { | ||
| + buffer.push("<p>" + I18n.t("admin.backups.logs.none") + "</p>"); | ||
| + } | ||
| + // add a loading indicator | ||
| + if (this.get("status.isOperationRunning")) { | ||
| + buffer.push(renderSpinner('small')); | ||
| + } | ||
| + } | ||
| +})); |
| @@ -0,0 +1,33 @@ | ||
| +import { iconHTML } from 'discourse-common/lib/icon-library'; | ||
| +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 }); | ||
| + } | ||
| + } | ||
| +})); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['row'] | ||
| +}); |
| @@ -0,0 +1,42 @@ | ||
| +import loadScript from 'discourse/lib/load-script'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'canvas', | ||
| + refreshChart(){ | ||
| + const ctx = this.$()[0].getContext("2d"); | ||
| + const model = this.get("model"); | ||
| + const rawData = this.get("model.data"); | ||
| + | ||
| + var data = { | ||
| + labels: rawData.map(r => r.x), | ||
| + datasets: [{ | ||
| + data: rawData.map(r => r.y), | ||
| + label: model.get('title'), | ||
| + backgroundColor: "rgba(200,220,240,0.3)", | ||
| + borderColor: "#08C" | ||
| + }] | ||
| + }; | ||
| + | ||
| + const config = { | ||
| + type: 'line', | ||
| + data: data, | ||
| + options: { | ||
| + responsive: true, | ||
| + scales: { | ||
| + yAxes: [{ | ||
| + display: true, | ||
| + ticks: { | ||
| + suggestedMin: 0 | ||
| + } | ||
| + }] | ||
| + } | ||
| + }, | ||
| + }; | ||
| + | ||
| + this._chart = new window.Chart(ctx, config); | ||
| + }, | ||
| + | ||
| + didInsertElement(){ | ||
| + loadScript("/javascripts/Chart.min.js").then(() => this.refreshChart.apply(this)); | ||
| + } | ||
| +}); |
| @@ -1,34 +0,0 @@ | ||
| -export default Ember.Component.extend({ | ||
| - tagName: 'div', | ||
| - | ||
| - didInsertElement: function(){ | ||
| - this.$("input").select2({ | ||
| - multiple: true, | ||
| - width: '100%', | ||
| - query: function(opts){ | ||
| - opts.callback({ | ||
| - results: this.get("available").map(this._format) | ||
| - }); | ||
| - }.bind(this) | ||
| - }).on("change", function(evt) { | ||
| - if (evt.added){ | ||
| - this.triggerAction({action: "groupAdded", | ||
| - actionContext: this.get("available" | ||
| - ).findBy("id", evt.added.id)}); | ||
| - } else if (evt.removed) { | ||
| - this.triggerAction({action:"groupRemoved", | ||
| - actionContext: this.get("selected" | ||
| - ).findBy("id", evt.removed.id)}); | ||
| - } | ||
| - }.bind(this)); | ||
| - this._refreshOnReset(); | ||
| - }, | ||
| - | ||
| - _format: function(item){ | ||
| - return {"text": item.name, "id": item.id, "locked": item.automatic}; | ||
| - }, | ||
| - | ||
| - _refreshOnReset: function() { | ||
| - this.$("input").select2("data", this.get("selected").map(this._format)); | ||
| - }.observes("selected") | ||
| -}); |
| @@ -1,18 +0,0 @@ | ||
| -export default Ember.Component.extend({ | ||
| - tagName: 'li', | ||
| - classNameBindings: ['active'], | ||
| - | ||
| - router: function() { | ||
| - return this.container.lookup('router:main'); | ||
| - }.property(), | ||
| - | ||
| - active: function() { | ||
| - const route = this.get('route'); | ||
| - if (!route) { return; } | ||
| - | ||
| - const routeParam = this.get('routeParam'), | ||
| - router = this.get('router'); | ||
| - | ||
| - return routeParam ? router.isActive(route, routeParam) : router.isActive(route); | ||
| - }.property('router.url', 'route') | ||
| -}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: '' | ||
| +}); |
| @@ -1,3 +1,5 @@ | ||
| export default Ember.Component.extend({ | ||
| - tagName: 'tbody' | ||
| + tagName: 'tr', | ||
| + reverseColors: Ember.computed.match('report.type', /^(time_to_first_response|topics_with_no_response)$/), | ||
| + classNameBindings: ['reverseColors'] | ||
| }); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'tr' | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'tr' | ||
| +}); |
| @@ -0,0 +1,98 @@ | ||
| +import UserField from 'admin/models/user-field'; | ||
| +import { bufferedProperty } from 'discourse/mixins/buffered-content'; | ||
| +import { popupAjaxError } from 'discourse/lib/ajax-error'; | ||
| +import { propertyEqual } from 'discourse/lib/computed'; | ||
| + | ||
| +export default Ember.Component.extend(bufferedProperty('userField'), { | ||
| + editing: Ember.computed.empty('userField.id'), | ||
| + classNameBindings: [':user-field'], | ||
| + | ||
| + cantMoveUp: propertyEqual('userField', 'firstField'), | ||
| + cantMoveDown: propertyEqual('userField', 'lastField'), | ||
| + | ||
| + userFieldsDescription: function() { | ||
| + return I18n.t('admin.user_fields.description'); | ||
| + }.property(), | ||
| + | ||
| + bufferedFieldType: function() { | ||
| + return UserField.fieldTypeById(this.get('buffered.field_type')); | ||
| + }.property('buffered.field_type'), | ||
| + | ||
| + _focusOnEdit: function() { | ||
| + if (this.get('editing')) { | ||
| + Ember.run.scheduleOnce('afterRender', this, '_focusName'); | ||
| + } | ||
| + }.observes('editing').on('didInsertElement'), | ||
| + | ||
| + _focusName: function() { | ||
| + $('.user-field-name').select(); | ||
| + }, | ||
| + | ||
| + fieldName: function() { | ||
| + return UserField.fieldTypeById(this.get('userField.field_type')).get('name'); | ||
| + }.property('userField.field_type'), | ||
| + | ||
| + flags: function() { | ||
| + const ret = []; | ||
| + if (this.get('userField.editable')) { | ||
| + ret.push(I18n.t('admin.user_fields.editable.enabled')); | ||
| + } | ||
| + if (this.get('userField.required')) { | ||
| + ret.push(I18n.t('admin.user_fields.required.enabled')); | ||
| + } | ||
| + if (this.get('userField.show_on_profile')) { | ||
| + ret.push(I18n.t('admin.user_fields.show_on_profile.enabled')); | ||
| + } | ||
| + if (this.get('userField.show_on_user_card')) { | ||
| + ret.push(I18n.t('admin.user_fields.show_on_user_card.enabled')); | ||
| + } | ||
| + | ||
| + return ret.join(', '); | ||
| + }.property('userField.editable', 'userField.required', 'userField.show_on_profile', 'userField.show_on_user_card'), | ||
| + | ||
| + actions: { | ||
| + save() { | ||
| + const self = this; | ||
| + const buffered = this.get('buffered'); | ||
| + const attrs = buffered.getProperties('name', | ||
| + 'description', | ||
| + 'field_type', | ||
| + 'editable', | ||
| + 'required', | ||
| + 'show_on_profile', | ||
| + 'show_on_user_card', | ||
| + 'options'); | ||
| + | ||
| + this.get('userField').save(attrs).then(function() { | ||
| + self.set('editing', false); | ||
| + self.commitBuffer(); | ||
| + }).catch(popupAjaxError); | ||
| + }, | ||
| + | ||
| + moveUp() { | ||
| + this.sendAction('moveUpAction', this.get('userField')); | ||
| + }, | ||
| + | ||
| + moveDown() { | ||
| + this.sendAction('moveDownAction', this.get('userField')); | ||
| + }, | ||
| + | ||
| + edit() { | ||
| + this.set('editing', true); | ||
| + }, | ||
| + | ||
| + destroy() { | ||
| + this.sendAction('destroyAction', this.get('userField')); | ||
| + }, | ||
| + | ||
| + cancel() { | ||
| + const id = this.get('userField.id'); | ||
| + if (Ember.isEmpty(id)) { | ||
| + this.sendAction('destroyAction', this.get('userField')); | ||
| + } else { | ||
| + this.rollbackBuffer(); | ||
| + this.set('editing', false); | ||
| + } | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,19 @@ | ||
| +import { iconHTML } from 'discourse-common/lib/icon-library'; | ||
| +import { bufferedRender } from 'discourse-common/lib/buffered-render'; | ||
| + | ||
| +export default Ember.Component.extend(bufferedRender({ | ||
| + classNames: ['watched-word'], | ||
| + | ||
| + buildBuffer(buffer) { | ||
| + buffer.push(iconHTML('times')); | ||
| + buffer.push(' ' + this.get('word.word')); | ||
| + }, | ||
| + | ||
| + click() { | ||
| + this.get('word').destroy().then(() => { | ||
| + this.sendAction('action', this.get('word')); | ||
| + }).catch(e => { | ||
| + bootbox.alert(I18n.t("generic_error_with_reason", {error: `http: ${e.status} - ${e.body}`})); | ||
| + });; | ||
| + } | ||
| +})); |
| @@ -0,0 +1,42 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['hook-event'], | ||
| + typeName: Ember.computed.alias('type.name'), | ||
| + | ||
| + @computed('typeName') | ||
| + name(typeName) { | ||
| + return I18n.t(`admin.web_hooks.${typeName}_event.name`); | ||
| + }, | ||
| + | ||
| + @computed('typeName') | ||
| + details(typeName) { | ||
| + return I18n.t(`admin.web_hooks.${typeName}_event.details`); | ||
| + }, | ||
| + | ||
| + @computed('model.[]', 'typeName') | ||
| + eventTypeExists(eventTypes, typeName) { | ||
| + return eventTypes.any(event => event.name === typeName); | ||
| + }, | ||
| + | ||
| + @computed('eventTypeExists') | ||
| + enabled: { | ||
| + get(eventTypeExists) { | ||
| + return eventTypeExists; | ||
| + }, | ||
| + set(value, eventTypeExists) { | ||
| + const type = this.get('type'); | ||
| + const model = this.get('model'); | ||
| + // add an association when not exists | ||
| + if (value !== eventTypeExists) { | ||
| + if (value) { | ||
| + model.addObject(type); | ||
| + } else { | ||
| + model.removeObjects(model.filter(eventType => eventType.name === type.name)); | ||
| + } | ||
| + } | ||
| + | ||
| + return value; | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,78 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| +import { ajax } from 'discourse/lib/ajax'; | ||
| +import { popupAjaxError } from 'discourse/lib/ajax-error'; | ||
| +import { ensureJSON, plainJSON, prettyJSON } from 'discourse/lib/formatter'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'li', | ||
| + expandDetails: null, | ||
| + | ||
| + @computed('model.status') | ||
| + statusColorClasses(status) { | ||
| + if (!status) return ''; | ||
| + | ||
| + if (status >= 200 && status <= 299) { | ||
| + return 'text-successful'; | ||
| + } else { | ||
| + return 'text-danger'; | ||
| + } | ||
| + }, | ||
| + | ||
| + @computed('model.created_at') | ||
| + createdAt(createdAt) { | ||
| + return moment(createdAt).format('YYYY-MM-DD HH:mm:ss'); | ||
| + }, | ||
| + | ||
| + @computed('model.duration') | ||
| + completion(duration) { | ||
| + const seconds = Math.floor(duration / 10.0) / 100.0; | ||
| + return I18n.t('admin.web_hooks.events.completed_in', { count: seconds }); | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + redeliver() { | ||
| + return bootbox.confirm(I18n.t('admin.web_hooks.events.redeliver_confirm'), I18n.t('no_value'), I18n.t('yes_value'), result => { | ||
| + if (result) { | ||
| + ajax(`/admin/api/web_hooks/${this.get('model.web_hook_id')}/events/${this.get('model.id')}/redeliver`, { type: 'POST' }).then(json => { | ||
| + this.set('model', json.web_hook_event); | ||
| + }).catch(popupAjaxError); | ||
| + } | ||
| + }); | ||
| + }, | ||
| + | ||
| + toggleRequest() { | ||
| + const expandDetailsKey = 'request'; | ||
| + | ||
| + if (this.get('expandDetails') !== expandDetailsKey) { | ||
| + let headers = _.extend({ | ||
| + 'Request URL': this.get('model.request_url'), | ||
| + 'Request method': 'POST' | ||
| + }, ensureJSON(this.get('model.headers'))); | ||
| + this.setProperties({ | ||
| + headers: plainJSON(headers), | ||
| + body: prettyJSON(this.get('model.payload')), | ||
| + expandDetails: expandDetailsKey, | ||
| + bodyLabel: I18n.t('admin.web_hooks.events.payload') | ||
| + }); | ||
| + } else { | ||
| + this.set('expandDetails', null); | ||
| + } | ||
| + }, | ||
| + | ||
| + toggleResponse() { | ||
| + const expandDetailsKey = 'response'; | ||
| + | ||
| + if (this.get('expandDetails') !== expandDetailsKey) { | ||
| + this.setProperties({ | ||
| + headers: plainJSON(this.get('model.response_headers')), | ||
| + body: this.get('model.response_body'), | ||
| + expandDetails: expandDetailsKey, | ||
| + bodyLabel: I18n.t('admin.web_hooks.events.body') | ||
| + }); | ||
| + } else { | ||
| + this.set('expandDetails', null); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| +}); |
| @@ -0,0 +1,28 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| +import { iconHTML } from 'discourse-common/lib/icon-library'; | ||
| +import { bufferedRender } from 'discourse-common/lib/buffered-render'; | ||
| + | ||
| +export default Ember.Component.extend(bufferedRender({ | ||
| + classes: ["text-muted", "text-danger", "text-successful"], | ||
| + icons: ["circle-o", "times-circle", "circle"], | ||
| + | ||
| + @computed('deliveryStatuses', 'model.last_delivery_status') | ||
| + status(deliveryStatuses, lastDeliveryStatus) { | ||
| + return deliveryStatuses.find(s => s.id === lastDeliveryStatus); | ||
| + }, | ||
| + | ||
| + @computed('status.id', 'icons') | ||
| + icon(statusId, icons) { | ||
| + return icons[statusId - 1]; | ||
| + }, | ||
| + | ||
| + @computed('status.id', 'classes') | ||
| + class(statusId, classes) { | ||
| + return classes[statusId - 1]; | ||
| + }, | ||
| + | ||
| + buildBuffer(buffer) { | ||
| + buffer.push(iconHTML(this.get('icon'), { class: this.get('class') })); | ||
| + buffer.push(I18n.t(`admin.web_hooks.delivery_status.${this.get('status.name')}`)); | ||
| + } | ||
| +})); |
| @@ -0,0 +1,11 @@ | ||
| +export default Ember.Component.extend({ | ||
| + didInsertElement() { | ||
| + this._super(); | ||
| + $('body').addClass('admin-interface'); | ||
| + }, | ||
| + | ||
| + willDestroyElement() { | ||
| + this._super(); | ||
| + $('body').removeClass('admin-interface'); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: '' | ||
| +}); |
| @@ -0,0 +1,64 @@ | ||
| +import { bufferedProperty } from 'discourse/mixins/buffered-content'; | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| +import { on, observes } from 'ember-addons/ember-computed-decorators'; | ||
| +import { popupAjaxError } from 'discourse/lib/ajax-error'; | ||
| + | ||
| +export default Ember.Component.extend(bufferedProperty('host'), { | ||
| + editToggled: false, | ||
| + tagName: 'tr', | ||
| + categoryId: null, | ||
| + | ||
| + editing: Ember.computed.or('host.isNew', 'editToggled'), | ||
| + | ||
| + @on('didInsertElement') | ||
| + @observes('editing') | ||
| + _focusOnInput() { | ||
| + Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); }); | ||
| + }, | ||
| + | ||
| + @computed('buffered.host', 'host.isSaving') | ||
| + cantSave(host, isSaving) { | ||
| + return isSaving || Ember.isEmpty(host); | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + edit() { | ||
| + this.set('categoryId', this.get('host.category.id')); | ||
| + this.set('editToggled', true); | ||
| + }, | ||
| + | ||
| + save() { | ||
| + if (this.get('cantSave')) { return; } | ||
| + | ||
| + const props = this.get('buffered').getProperties('host', 'path_whitelist', 'class_name'); | ||
| + props.category_id = this.get('categoryId'); | ||
| + | ||
| + const host = this.get('host'); | ||
| + | ||
| + host.save(props).then(() => { | ||
| + host.set('category', Discourse.Category.findById(this.get('categoryId'))); | ||
| + this.set('editToggled', false); | ||
| + }).catch(popupAjaxError); | ||
| + }, | ||
| + | ||
| + delete() { | ||
| + bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => { | ||
| + if (result) { | ||
| + this.get('host').destroyRecord().then(() => { | ||
| + this.sendAction('deleteHost', this.get('host')); | ||
| + }); | ||
| + } | ||
| + }); | ||
| + }, | ||
| + | ||
| + cancel() { | ||
| + const host = this.get('host'); | ||
| + if (host.get('isNew')) { | ||
| + this.sendAction('deleteHost', host); | ||
| + } else { | ||
| + this.rollbackBuffer(); | ||
| + this.set('editToggled', false); | ||
| + } | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,23 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['embed-setting'], | ||
| + | ||
| + @computed('field') | ||
| + inputId(field) { return field.dasherize(); }, | ||
| + | ||
| + @computed('field') | ||
| + translationKey(field) { return `admin.embedding.${field}`; }, | ||
| + | ||
| + @computed('type') | ||
| + isCheckbox(type) { return type === "checkbox"; }, | ||
| + | ||
| + @computed('value') | ||
| + checked: { | ||
| + get(value) { return !!value; }, | ||
| + set(value) { | ||
| + this.set('value', value); | ||
| + return value; | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['flag-user-lists'] | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['flagged-post-response'] | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'h3' | ||
| +}); |
| @@ -0,0 +1,56 @@ | ||
| +import showModal from 'discourse/lib/show-modal'; | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + adminTools: Ember.inject.service(), | ||
| + expanded: false, | ||
| + tagName: 'div', | ||
| + classNameBindings: [ | ||
| + ':flagged-post', | ||
| + 'flaggedPost.hidden:hidden-post', | ||
| + 'flaggedPost.deleted' | ||
| + ], | ||
| + | ||
| + canAct: Ember.computed.alias('actableFilter'), | ||
| + | ||
| + @computed('filter') | ||
| + actableFilter(filter) { | ||
| + return filter === 'active'; | ||
| + }, | ||
| + | ||
| + removeAfter(promise) { | ||
| + return promise.then(() => this.attrs.removePost()); | ||
| + }, | ||
| + | ||
| + _spawnModal(name, model, modalClass) { | ||
| + let controller = showModal(name, { model, admin: true, modalClass }); | ||
| + controller.removeAfter = (p) => this.removeAfter(p); | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + removeAfter(promise) { | ||
| + return this.removeAfter(promise); | ||
| + }, | ||
| + | ||
| + disagree() { | ||
| + this.removeAfter(this.get('flaggedPost').disagreeFlags()); | ||
| + }, | ||
| + | ||
| + defer() { | ||
| + this.removeAfter(this.get('flaggedPost').deferFlags()); | ||
| + }, | ||
| + | ||
| + expand() { | ||
| + this.get('flaggedPost').expandHidden().then(() => { | ||
| + this.set('expanded', true); | ||
| + }); | ||
| + }, | ||
| + | ||
| + showModerationHistory() { | ||
| + this.get('adminTools').showModerationHistory({ | ||
| + filter: 'post', | ||
| + post_id: this.get('flaggedPost.id') | ||
| + }); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,12 @@ | ||
| +import { on, observes } from 'ember-addons/ember-computed-decorators'; | ||
| +import highlightSyntax from 'discourse/lib/highlight-syntax'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + | ||
| + @on('didInsertElement') | ||
| + @observes('code') | ||
| + _refresh: function() { | ||
| + highlightSyntax(this.$()); | ||
| + } | ||
| + | ||
| +}); |
| @@ -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(); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -1,53 +0,0 @@ | ||
| -/** | ||
| - Provide a nice GUI for a pipe-delimited list in the site settings. | ||
| - | ||
| - @param settingValue is a reference to SiteSetting.value. | ||
| - @param choices is a reference to SiteSetting.choices | ||
| -**/ | ||
| -export default Ember.Component.extend({ | ||
| - | ||
| - _select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) { | ||
| - var text = selectedObject.text; | ||
| - if (text.length <= 6) { | ||
| - jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text}); | ||
| - } | ||
| - return htmlEscaper(text); | ||
| - }, | ||
| - | ||
| - _initializeSelect2: function(){ | ||
| - var options = { | ||
| - multiple: false, | ||
| - separator: "|", | ||
| - tokenSeparators: ["|"], | ||
| - tags : this.get("choices") || [], | ||
| - width: 'off', | ||
| - dropdownCss: this.get("choices") ? {} : {display: 'none'} | ||
| - }; | ||
| - | ||
| - var settingName = this.get('settingName'); | ||
| - if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) { | ||
| - options.formatSelection = this._select2FormatSelection; | ||
| - } | ||
| - | ||
| - var self = this; | ||
| - this.$("input").select2(options).on("change", function(obj) { | ||
| - self.set("settingValue", obj.val.join("|")); | ||
| - self.refreshSortables(); | ||
| - }); | ||
| - | ||
| - this.refreshSortables(); | ||
| - }.on('didInsertElement'), | ||
| - | ||
| - refreshOnReset: function() { | ||
| - this.$("input").select2("val", this.get("settingValue").split("|")); | ||
| - }.observes("settingValue"), | ||
| - | ||
| - refreshSortables: function() { | ||
| - var self = this; | ||
| - this.$("ul.select2-choices").sortable().on('sortupdate', function() { | ||
| - self.$("input").select2("onSortEnd"); | ||
| - }); | ||
| - } | ||
| -}); | ||
| - | ||
| - |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: 'tr', | ||
| +}); |
| @@ -0,0 +1,32 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +const ACTIONS = ['delete', 'edit', 'none']; | ||
| +export default Ember.Component.extend({ | ||
| + postAction: null, | ||
| + postEdit: null, | ||
| + | ||
| + @computed | ||
| + penaltyActions() { | ||
| + return ACTIONS.map(id => { | ||
| + return { id, name: I18n.t(`admin.user.penalty_post_${id}`) }; | ||
| + }); | ||
| + }, | ||
| + | ||
| + editing: Ember.computed.equal('postAction', 'edit'), | ||
| + | ||
| + actions: { | ||
| + penaltyChanged() { | ||
| + let postAction = this.get('postAction'); | ||
| + | ||
| + // If we switch to edit mode, jump to the edit textarea | ||
| + if (postAction === 'edit') { | ||
| + Ember.run.scheduleOnce('afterRender', () => { | ||
| + let $elem = this.$(); | ||
| + let body = $elem.closest('.modal-body'); | ||
| + body.scrollTop(body.height()); | ||
| + $elem.find('.post-editor').focus(); | ||
| + }); | ||
| + } | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,58 @@ | ||
| +import Permalink from 'admin/models/permalink'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['permalink-form'], | ||
| + formSubmitted: false, | ||
| + permalinkType: 'topic_id', | ||
| + | ||
| + permalinkTypes: function() { | ||
| + return [ | ||
| + {id: 'topic_id', name: I18n.t('admin.permalink.topic_id')}, | ||
| + {id: 'post_id', name: I18n.t('admin.permalink.post_id')}, | ||
| + {id: 'category_id', name: I18n.t('admin.permalink.category_id')}, | ||
| + {id: 'external_url', name: I18n.t('admin.permalink.external_url')} | ||
| + ]; | ||
| + }.property(), | ||
| + | ||
| + permalinkTypePlaceholder: function() { | ||
| + return 'admin.permalink.' + this.get('permalinkType'); | ||
| + }.property('permalinkType'), | ||
| + | ||
| + actions: { | ||
| + submit: function() { | ||
| + if (!this.get('formSubmitted')) { | ||
| + const self = this; | ||
| + self.set('formSubmitted', true); | ||
| + const permalink = Permalink.create({url: self.get('url'), permalink_type: self.get('permalinkType'), permalink_type_value: self.get('permalink_type_value')}); | ||
| + permalink.save().then(function(result) { | ||
| + self.set('url', ''); | ||
| + self.set('permalink_type_value', ''); | ||
| + self.set('formSubmitted', false); | ||
| + self.sendAction('action', Permalink.create(result.permalink)); | ||
| + Em.run.schedule('afterRender', function() { self.$('.permalink-url').focus(); }); | ||
| + }, function(e) { | ||
| + self.set('formSubmitted', false); | ||
| + let error; | ||
| + if (e.responseJSON && e.responseJSON.errors) { | ||
| + error = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}); | ||
| + } else { | ||
| + error = I18n.t("generic_error"); | ||
| + } | ||
| + bootbox.alert(error, function() { self.$('.permalink-url').focus(); }); | ||
| + }); | ||
| + } | ||
| + } | ||
| + }, | ||
| + | ||
| + didInsertElement: function() { | ||
| + var self = this; | ||
| + self._super(); | ||
| + Em.run.schedule('afterRender', function() { | ||
| + self.$('.external-url').keydown(function(e) { | ||
| + if (e.keyCode === 13) { // enter key | ||
| + self.send('submit'); | ||
| + } | ||
| + }); | ||
| + }); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,123 @@ | ||
| +import { iconHTML } from 'discourse-common/lib/icon-library'; | ||
| +import { bufferedRender } from 'discourse-common/lib/buffered-render'; | ||
| + | ||
| +/*global Resumable:true */ | ||
| + | ||
| +/** | ||
| + Example usage: | ||
| + | ||
| + {{resumable-upload | ||
| + target="/admin/backups/upload" | ||
| + success="successAction" | ||
| + error="errorAction" | ||
| + uploadText="UPLOAD" | ||
| + }} | ||
| +**/ | ||
| +export default Ember.Component.extend(bufferedRender({ | ||
| + tagName: "button", | ||
| + classNames: ["btn", "ru"], | ||
| + classNameBindings: ["isUploading"], | ||
| + attributeBindings: ["translatedTitle:title"], | ||
| + | ||
| + resumable: null, | ||
| + | ||
| + isUploading: false, | ||
| + progress: 0, | ||
| + | ||
| + rerenderTriggers: ['isUploading', 'progress'], | ||
| + | ||
| + translatedTitle: function() { | ||
| + const title = this.get('title'); | ||
| + return title ? I18n.t(title) : this.get('text'); | ||
| + }.property('title', 'text'), | ||
| + | ||
| + text: function() { | ||
| + if (this.get("isUploading")) { | ||
| + return this.get("progress") + " %"; | ||
| + } else { | ||
| + return this.get("uploadText"); | ||
| + } | ||
| + }.property("isUploading", "progress"), | ||
| + | ||
| + buildBuffer(buffer) { | ||
| + const icon = this.get("isUploading") ? "times" : "upload"; | ||
| + buffer.push(iconHTML(icon)); | ||
| + buffer.push("<span class='ru-label'>" + this.get("text") + "</span>"); | ||
| + buffer.push("<span class='ru-progress' style='width:" + this.get("progress") + "%'></span>"); | ||
| + }, | ||
| + | ||
| + click: function() { | ||
| + if (this.get("isUploading")) { | ||
| + this.resumable.cancel(); | ||
| + var self = this; | ||
| + Em.run.later(function() { self._reset(); }); | ||
| + return false; | ||
| + } else { | ||
| + return true; | ||
| + } | ||
| + }, | ||
| + | ||
| + _reset: function() { | ||
| + this.setProperties({ isUploading: false, progress: 0 }); | ||
| + }, | ||
| + | ||
| + _initialize: function() { | ||
| + this.resumable = new Resumable({ | ||
| + target: Discourse.getURL(this.get("target")), | ||
| + maxFiles: 1, // only 1 file at a time | ||
| + headers: { "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") } | ||
| + }); | ||
| + | ||
| + var self = this; | ||
| + | ||
| + this.resumable.on("fileAdded", function() { | ||
| + // automatically upload the selected file | ||
| + self.resumable.upload(); | ||
| + // mark as uploading | ||
| + Em.run.later(function() { | ||
| + self.set("isUploading", true); | ||
| + }); | ||
| + }); | ||
| + | ||
| + this.resumable.on("fileProgress", function(file) { | ||
| + // update progress | ||
| + Em.run.later(function() { | ||
| + self.set("progress", parseInt(file.progress() * 100, 10)); | ||
| + }); | ||
| + }); | ||
| + | ||
| + this.resumable.on("fileSuccess", function(file) { | ||
| + Em.run.later(function() { | ||
| + // mark as not uploading anymore | ||
| + self._reset(); | ||
| + // fire an event to allow the parent route to reload its model | ||
| + self.sendAction("success", file.fileName); | ||
| + }); | ||
| + }); | ||
| + | ||
| + this.resumable.on("fileError", function(file, message) { | ||
| + Em.run.later(function() { | ||
| + // mark as not uploading anymore | ||
| + self._reset(); | ||
| + // fire an event to allow the parent route to display the error message | ||
| + self.sendAction("error", file.fileName, message); | ||
| + }); | ||
| + }); | ||
| + | ||
| + }.on("init"), | ||
| + | ||
| + _assignBrowse: function() { | ||
| + var self = this; | ||
| + Em.run.schedule("afterRender", function() { | ||
| + self.resumable.assignBrowse(self.$()); | ||
| + }); | ||
| + }.on("didInsertElement"), | ||
| + | ||
| + _teardown: function() { | ||
| + if (this.resumable) { | ||
| + this.resumable.cancel(); | ||
| + this.resumable = null; | ||
| + } | ||
| + }.on("willDestroyElement") | ||
| + | ||
| +})); |
| @@ -1,114 +0,0 @@ | ||
| -/*global Resumable:true */ | ||
| - | ||
| -/** | ||
| - Example usage: | ||
| - | ||
| - {{resumable-upload | ||
| - target="/admin/backups/upload" | ||
| - success="successAction" | ||
| - error="errorAction" | ||
| - uploadText="UPLOAD" | ||
| - }} | ||
| -**/ | ||
| -Discourse.ResumableUploadComponent = Ember.Component.extend(Discourse.StringBuffer, { | ||
| - tagName: "button", | ||
| - classNames: ["btn", "ru"], | ||
| - classNameBindings: ["isUploading"], | ||
| - | ||
| - resumable: null, | ||
| - | ||
| - isUploading: false, | ||
| - progress: 0, | ||
| - | ||
| - rerenderTriggers: ['isUploading', 'progress'], | ||
| - | ||
| - text: function() { | ||
| - if (this.get("isUploading")) { | ||
| - return this.get("progress") + " %"; | ||
| - } else { | ||
| - return this.get("uploadText"); | ||
| - } | ||
| - }.property("isUploading", "progress"), | ||
| - | ||
| - renderString: function(buffer) { | ||
| - var icon = this.get("isUploading") ? "times" : "upload"; | ||
| - buffer.push("<i class='fa fa-" + icon + "'></i>"); | ||
| - buffer.push("<span class='ru-label'>" + this.get("text") + "</span>"); | ||
| - buffer.push("<span class='ru-progress' style='width:" + this.get("progress") + "%'></span>"); | ||
| - }, | ||
| - | ||
| - click: function() { | ||
| - if (this.get("isUploading")) { | ||
| - this.resumable.cancel(); | ||
| - var self = this; | ||
| - Em.run.later(function() { self._reset(); }); | ||
| - return false; | ||
| - } else { | ||
| - return true; | ||
| - } | ||
| - }, | ||
| - | ||
| - _reset: function() { | ||
| - this.setProperties({ isUploading: false, progress: 0 }); | ||
| - }, | ||
| - | ||
| - _initialize: function() { | ||
| - this.resumable = new Resumable({ | ||
| - target: this.get("target"), | ||
| - maxFiles: 1, // only 1 file at a time | ||
| - headers: { "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") } | ||
| - }); | ||
| - | ||
| - var self = this; | ||
| - | ||
| - this.resumable.on("fileAdded", function() { | ||
| - // automatically upload the selected file | ||
| - self.resumable.upload(); | ||
| - // mark as uploading | ||
| - Em.run.later(function() { | ||
| - self.set("isUploading", true); | ||
| - }); | ||
| - }); | ||
| - | ||
| - this.resumable.on("fileProgress", function(file) { | ||
| - // update progress | ||
| - Em.run.later(function() { | ||
| - self.set("progress", parseInt(file.progress() * 100, 10)); | ||
| - }); | ||
| - }); | ||
| - | ||
| - this.resumable.on("fileSuccess", function(file) { | ||
| - Em.run.later(function() { | ||
| - // mark as not uploading anymore | ||
| - self._reset(); | ||
| - // fire an event to allow the parent route to reload its model | ||
| - self.sendAction("success", file.fileName); | ||
| - }); | ||
| - }); | ||
| - | ||
| - this.resumable.on("fileError", function(file, message) { | ||
| - Em.run.later(function() { | ||
| - // mark as not uploading anymore | ||
| - self._reset(); | ||
| - // fire an event to allow the parent route to display the error message | ||
| - self.sendAction("error", file.fileName, message); | ||
| - }); | ||
| - }); | ||
| - | ||
| - }.on("init"), | ||
| - | ||
| - _assignBrowse: function() { | ||
| - var self = this; | ||
| - Em.run.schedule("afterRender", function() { | ||
| - self.resumable.assignBrowse(self.$()); | ||
| - }); | ||
| - }.on("didInsertElement"), | ||
| - | ||
| - _teardown: function() { | ||
| - if (this.resumable) { | ||
| - this.resumable.cancel(); | ||
| - this.resumable = null; | ||
| - } | ||
| - }.on("willDestroyElement") | ||
| - | ||
| -}); |
| @@ -0,0 +1,18 @@ | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['controls'], | ||
| + | ||
| + buttonDisabled: Ember.computed.or('model.isSaving', 'saveDisabled'), | ||
| + | ||
| + @computed('model.isSaving') | ||
| + savingText(saving) { | ||
| + return saving ? 'saving' : 'save'; | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + saveChanges() { | ||
| + this.sendAction(); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,79 @@ | ||
| +/** | ||
| + A form to create an IP address that will be blocked or whitelisted. | ||
| + Example usage: | ||
| + | ||
| + {{screened-ip-address-form action="recordAdded"}} | ||
| + | ||
| + where action is a callback on the controller or route that will get called after | ||
| + the new record is successfully saved. It is called with the new ScreenedIpAddress record | ||
| + as an argument. | ||
| +**/ | ||
| + | ||
| +import ScreenedIpAddress from 'admin/models/screened-ip-address'; | ||
| +import computed from 'ember-addons/ember-computed-decorators'; | ||
| +import { on } from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['screened-ip-address-form'], | ||
| + formSubmitted: false, | ||
| + actionName: 'block', | ||
| + | ||
| + @computed | ||
| + adminWhitelistEnabled() { | ||
| + return Discourse.SiteSettings.use_admin_ip_whitelist; | ||
| + }, | ||
| + | ||
| + @computed("adminWhitelistEnabled") | ||
| + actionNames(adminWhitelistEnabled) { | ||
| + if (adminWhitelistEnabled) { | ||
| + return [ | ||
| + {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')}, | ||
| + {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}, | ||
| + {id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')} | ||
| + ]; | ||
| + } else { | ||
| + return [ | ||
| + {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')}, | ||
| + {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')} | ||
| + ]; | ||
| + } | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + submit() { | ||
| + if (!this.get('formSubmitted')) { | ||
| + this.set('formSubmitted', true); | ||
| + const screenedIpAddress = ScreenedIpAddress.create({ | ||
| + ip_address: this.get('ip_address'), | ||
| + action_name: this.get('actionName') | ||
| + }); | ||
| + screenedIpAddress.save().then(result => { | ||
| + if (result.success) { | ||
| + this.setProperties({ ip_address: '', formSubmitted: false }); | ||
| + this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address)); | ||
| + Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus()); | ||
| + } else { | ||
| + bootbox.alert(result.errors); | ||
| + } | ||
| + }).catch(e => { | ||
| + this.set('formSubmitted', false); | ||
| + const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ? | ||
| + I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) : | ||
| + I18n.t("generic_error"); | ||
| + bootbox.alert(msg, () => this.$('.ip-address-input').focus()); | ||
| + }); | ||
| + } | ||
| + } | ||
| + }, | ||
| + | ||
| + @on("didInsertElement") | ||
| + _init() { | ||
| + Ember.run.schedule('afterRender', () => { | ||
| + this.$('.ip-address-input').keydown(e => { | ||
| + if (e.keyCode === 13) { | ||
| + this.send('submit'); | ||
| + } | ||
| + }); | ||
| + }); | ||
| + } | ||
| +}); |
| @@ -1,65 +0,0 @@ | ||
| -/** | ||
| - A form to create an IP address that will be blocked or whitelisted. | ||
| - Example usage: | ||
| - | ||
| - {{screened-ip-address-form action="recordAdded"}} | ||
| - | ||
| - where action is a callback on the controller or route that will get called after | ||
| - the new record is successfully saved. It is called with the new ScreenedIpAddress record | ||
| - as an argument. | ||
| - | ||
| - @class ScreenedIpAddressFormComponent | ||
| - @extends Ember.Component | ||
| - @namespace Discourse | ||
| - @module Discourse | ||
| -**/ | ||
| -Discourse.ScreenedIpAddressFormComponent = Ember.Component.extend({ | ||
| - classNames: ['screened-ip-address-form'], | ||
| - formSubmitted: false, | ||
| - actionName: 'block', | ||
| - | ||
| - actionNames: function() { | ||
| - return [ | ||
| - {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')}, | ||
| - {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}, | ||
| - {id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')} | ||
| - ]; | ||
| - }.property(), | ||
| - | ||
| - actions: { | ||
| - submit: function() { | ||
| - if (!this.get('formSubmitted')) { | ||
| - var self = this; | ||
| - this.set('formSubmitted', true); | ||
| - var screenedIpAddress = Discourse.ScreenedIpAddress.create({ip_address: this.get('ip_address'), action_name: this.get('actionName')}); | ||
| - screenedIpAddress.save().then(function(result) { | ||
| - self.set('ip_address', ''); | ||
| - self.set('formSubmitted', false); | ||
| - self.sendAction('action', Discourse.ScreenedIpAddress.create(result.screened_ip_address)); | ||
| - Em.run.schedule('afterRender', function() { self.$('.ip-address-input').focus(); }); | ||
| - }, function(e) { | ||
| - self.set('formSubmitted', false); | ||
| - var msg; | ||
| - if (e.responseJSON && e.responseJSON.errors) { | ||
| - msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}); | ||
| - } else { | ||
| - msg = I18n.t("generic_error"); | ||
| - } | ||
| - bootbox.alert(msg, function() { self.$('.ip-address-input').focus(); }); | ||
| - }); | ||
| - } | ||
| - } | ||
| - }, | ||
| - | ||
| - didInsertElement: function() { | ||
| - var self = this; | ||
| - this._super(); | ||
| - Em.run.schedule('afterRender', function() { | ||
| - self.$('.ip-address-input').keydown(function(e) { | ||
| - if (e.keyCode === 13) { // enter key | ||
| - self.send('submit'); | ||
| - } | ||
| - }); | ||
| - }); | ||
| - } | ||
| -}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: '' | ||
| +}); |
| @@ -0,0 +1,10 @@ | ||
| +import BufferedContent from 'discourse/mixins/buffered-content'; | ||
| +import SiteSetting from 'admin/models/site-setting'; | ||
| +import SettingComponent from 'admin/mixins/setting-component'; | ||
| + | ||
| +export default Ember.Component.extend(BufferedContent, SettingComponent, { | ||
| + _save() { | ||
| + const setting = this.get('buffered'); | ||
| + return SiteSetting.update(setting.get('setting'), setting.get('value')); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,17 @@ | ||
| +import computed from "ember-addons/ember-computed-decorators"; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + | ||
| + @computed("value") | ||
| + enabled: { | ||
| + get(value) { | ||
| + if (Ember.isEmpty(value)) { return false; } | ||
| + return value.toString() === "true"; | ||
| + }, | ||
| + set(value) { | ||
| + this.set("value", value ? "true" : "false"); | ||
| + return value; | ||
| + } | ||
| + }, | ||
| + | ||
| +}); |
| @@ -0,0 +1,16 @@ | ||
| +import computed from "ember-addons/ember-computed-decorators"; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + | ||
| + @computed("value") | ||
| + selectedCategories: { | ||
| + get(value) { | ||
| + return Discourse.Category.findByIds(value.split("|")); | ||
| + }, | ||
| + set(value) { | ||
| + this.set("value", value.mapBy("id").join("|")); | ||
| + return value; | ||
| + } | ||
| + } | ||
| + | ||
| +}); |
| @@ -0,0 +1,25 @@ | ||
| +import { on } from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['site-text'], | ||
| + classNameBindings: ['siteText.overridden'], | ||
| + | ||
| + @on('didInsertElement') | ||
| + highlightTerm() { | ||
| + const term = this.get('term'); | ||
| + if (term) { | ||
| + this.$('.site-text-id, .site-text-value').highlight(term, {className: 'text-highlight'}); | ||
| + } | ||
| + this.$('.site-text-value').ellipsis(); | ||
| + }, | ||
| + | ||
| + click() { | ||
| + this.send('edit'); | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + edit() { | ||
| + this.sendAction('editAction', this.get('siteText')); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,22 @@ | ||
| +import DiscourseURL from 'discourse/lib/url'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['table', 'staff-actions'], | ||
| + | ||
| + willDestroyElement() { | ||
| + this.$().off('click.discourse-staff-logs'); | ||
| + }, | ||
| + | ||
| + didInsertElement() { | ||
| + this._super(); | ||
| + | ||
| + this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => { | ||
| + let postId = $(e.target).attr('data-link-post-id'); | ||
| + | ||
| + this.store.find('post', postId).then(p => { | ||
| + DiscourseURL.routeTo(p.get('url')); | ||
| + }); | ||
| + return false; | ||
| + }); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,3 @@ | ||
| +export default Ember.Component.extend({ | ||
| + tagName: '' | ||
| +}); |
| @@ -0,0 +1,9 @@ | ||
| +import BufferedContent from 'discourse/mixins/buffered-content'; | ||
| +import SettingComponent from 'admin/mixins/setting-component'; | ||
| + | ||
| +export default Ember.Component.extend(BufferedContent, SettingComponent, { | ||
| + layoutName: 'admin/templates/components/site-setting', | ||
| + _save() { | ||
| + return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value')); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,104 @@ | ||
| +export default Ember.Component.extend({ | ||
| + classNameBindings: [':value-list'], | ||
| + | ||
| + _enableSorting: function() { | ||
| + const self = this; | ||
| + const placeholder = document.createElement("div"); | ||
| + placeholder.className = "placeholder"; | ||
| + | ||
| + let dragging = null; | ||
| + let over = null; | ||
| + let nodePlacement; | ||
| + | ||
| + this.$().on('dragstart.discourse', '.values .value', function(e) { | ||
| + dragging = e.currentTarget; | ||
| + e.dataTransfer.effectAllowed = 'move'; | ||
| + e.dataTransfer.setData("text/html", e.currentTarget); | ||
| + }); | ||
| + | ||
| + this.$().on('dragend.discourse', '.values .value', function() { | ||
| + Ember.run(function() { | ||
| + dragging.parentNode.removeChild(placeholder); | ||
| + dragging.style.display = 'block'; | ||
| + | ||
| + // Update data | ||
| + const from = Number(dragging.dataset.index); | ||
| + let to = Number(over.dataset.index); | ||
| + if (from < to) to--; | ||
| + if (nodePlacement === "after") to++; | ||
| + | ||
| + const collection = self.get('collection'); | ||
| + const fromObj = collection.objectAt(from); | ||
| + collection.replace(from, 1); | ||
| + collection.replace(to, 0, [fromObj]); | ||
| + self._saveValues(); | ||
| + }); | ||
| + return false; | ||
| + }); | ||
| + | ||
| + this.$().on('dragover.discourse', '.values', function(e) { | ||
| + e.preventDefault(); | ||
| + dragging.style.display = 'none'; | ||
| + if (e.target.className === "placeholder") { return; } | ||
| + over = e.target; | ||
| + | ||
| + const relY = e.originalEvent.clientY - over.offsetTop; | ||
| + const height = over.offsetHeight / 2; | ||
| + const parent = e.target.parentNode; | ||
| + | ||
| + if (relY > height) { | ||
| + nodePlacement = "after"; | ||
| + parent.insertBefore(placeholder, e.target.nextElementSibling); | ||
| + } else if(relY < height) { | ||
| + nodePlacement = "before"; | ||
| + parent.insertBefore(placeholder, e.target); | ||
| + } | ||
| + }); | ||
| + }.on('didInsertElement'), | ||
| + | ||
| + _removeSorting: function() { | ||
| + this.$().off('dragover.discourse').off('dragend.discourse').off('dragstart.discourse'); | ||
| + }.on('willDestroyElement'), | ||
| + | ||
| + _setupCollection: function() { | ||
| + const values = this.get('values'); | ||
| + if (this.get('inputType') === "array") { | ||
| + this.set('collection', values || []); | ||
| + } else { | ||
| + this.set('collection', (values && values.length) ? values.split("\n") : []); | ||
| + } | ||
| + }.on('init').observes('values'), | ||
| + | ||
| + _saveValues: function() { | ||
| + if (this.get('inputType') === "array") { | ||
| + this.set('values', this.get('collection')); | ||
| + } else { | ||
| + this.set('values', this.get('collection').join("\n")); | ||
| + } | ||
| + }, | ||
| + | ||
| + inputInvalid: Ember.computed.empty('newValue'), | ||
| + | ||
| + keyDown(e) { | ||
| + if (e.keyCode === 13) { | ||
| + this.send('addValue'); | ||
| + } | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + addValue() { | ||
| + if (this.get('inputInvalid')) { return; } | ||
| + | ||
| + this.get('collection').addObject(this.get('newValue')); | ||
| + this.set('newValue', ''); | ||
| + | ||
| + this._saveValues(); | ||
| + }, | ||
| + | ||
| + removeValue(value) { | ||
| + const collection = this.get('collection'); | ||
| + collection.removeObject(value); | ||
| + this._saveValues(); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -0,0 +1,55 @@ | ||
| +import WatchedWord from 'admin/models/watched-word'; | ||
| +import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; | ||
| + | ||
| +export default Ember.Component.extend({ | ||
| + classNames: ['watched-word-form'], | ||
| + formSubmitted: false, | ||
| + actionKey: null, | ||
| + showSuccessMessage: false, | ||
| + | ||
| + @computed('regularExpressions') | ||
| + placeholderKey(regularExpressions) { | ||
| + return "admin.watched_words.form.placeholder" + | ||
| + (regularExpressions ? "_regexp" : ""); | ||
| + }, | ||
| + | ||
| + @observes('word') | ||
| + removeSuccessMessage() { | ||
| + if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) { | ||
| + this.set('showSuccessMessage', false); | ||
| + } | ||
| + }, | ||
| + | ||
| + actions: { | ||
| + submit() { | ||
| + if (!this.get('formSubmitted')) { | ||
| + this.set('formSubmitted', true); | ||
| + | ||
| + const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') }); | ||
| + | ||
| + watchedWord.save().then(result => { | ||
| + this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true }); | ||
| + this.sendAction('action', WatchedWord.create(result)); | ||
| + Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus()); | ||
| + }).catch(e => { | ||
| + this.set('formSubmitted', false); | ||
| + const msg = (e.responseJSON && e.responseJSON.errors) ? | ||
| + I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) : | ||
| + I18n.t("generic_error"); | ||
| + bootbox.alert(msg, () => this.$('.watched-word-input').focus()); | ||
| + }); | ||
| + } | ||
| + } | ||
| + }, | ||
| + | ||
| + @on("didInsertElement") | ||
| + _init() { | ||
| + Ember.run.schedule('afterRender', () => { | ||
| + this.$('.watched-word-input').keydown(e => { | ||
| + if (e.keyCode === 13) { | ||
| + this.send('submit'); | ||
| + } | ||
| + }); | ||
| + }); | ||
| + } | ||
| +}); |
| @@ -0,0 +1,25 @@ | ||
| +import computed from "ember-addons/ember-computed-decorators"; | ||
| +import UploadMixin from "discourse/mixins/upload"; | ||
| + | ||
| +export default Em.Component.extend(UploadMixin, { | ||
| + type: 'csv', | ||
| + classNames: 'watched-words-uploader', | ||
| + uploadUrl: '/admin/logs/watched_words/upload', | ||
| + addDisabled: Em.computed.alias("uploading"), | ||
| + | ||
| + validateUploadedFilesOptions() { | ||
| + return { csvOnly: true }; | ||
| + }, | ||
| + | ||
| + @computed('actionKey') | ||
| + data(actionKey) { | ||
| + return { action_key: actionKey }; | ||
| + }, | ||
| + | ||
| + uploadDone() { | ||
| + if (this) { | ||
| + bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); | ||
| + this.sendAction("done"); | ||
| + } | ||
| + } | ||
| +}); |
| @@ -1,26 +0,0 @@ | ||
| -import ModalFunctionality from 'discourse/mixins/modal-functionality'; | ||
| -import ObjectController from 'discourse/controllers/object'; | ||
| - | ||
| -export default ObjectController.extend(ModalFunctionality, { | ||
| - needs: ["admin-flags-list"], | ||
| - | ||
| - _agreeFlag: function (actionOnPost) { | ||
| - var adminFlagController = this.get("controllers.admin-flags-list"); | ||
| - var post = this.get("content"); | ||
| - var self = this; | ||
| - | ||
| - return post.agreeFlags(actionOnPost).then(function () { | ||
| - adminFlagController.removeObject(post); | ||
| - self.send("closeModal"); | ||
| - }, function () { | ||
| - bootbox.alert(I18n.t("admin.flags.error")); | ||
| - }); | ||
| - }, | ||
| - | ||
| - actions: { | ||
| - agreeFlagHidePost: function () { return this._agreeFlag("hide"); }, | ||
| - agreeFlagKeepPost: function () { return this._agreeFlag("keep"); }, | ||
| - agreeFlagRestorePost: function () { return this._agreeFlag("restore"); } | ||
| - } | ||
| - | ||
| -}); |
| @@ -0,0 +1,32 @@ | ||
| +import ApiKey from 'admin/models/api-key'; | ||
| + | ||
| +export default Ember.Controller.extend({ | ||
| + | ||
| + actions: { | ||
| + generateMasterKey() { | ||
| + ApiKey.generateMasterKey().then(key => this.get('model').pushObject(key)); | ||
| + }, | ||
| + | ||
| + regenerateKey(key) { | ||
| + bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), result => { | ||
| + if (result) { | ||
| + key.regenerate(); | ||
| + } | ||
| + }); | ||
| + }, | ||
| + | ||
| + revokeKey(key) { | ||
| + bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), result => { | ||
| + if (result) { | ||
| + key.revoke().then(() => this.get('model').removeObject(key)); | ||
| + } | ||
| + }); | ||
| + } | ||
| + }, | ||
| + | ||
| + // Has a master key already been generated? | ||
| + hasMasterKey: function() { | ||
| + return !!this.get('model').findBy('user', null); | ||
| + }.property('model.[]') | ||
| + | ||
| +}); |