diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml index a915ea7f..acb85230 100644 --- a/.github/workflows/plugin-linting.yml +++ b/.github/workflows/plugin-linting.yml @@ -3,54 +3,52 @@ name: Linting on: push: branches: - - master - main - stable pull_request: - schedule: - - cron: '0 0 * * *' + +concurrency: + group: plugin-linting-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 16 + cache: yarn + + - name: Yarn install + run: yarn install - name: Set up ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 - - - name: Setup bundler - run: gem install bundler -v 2.1.4 --no-doc - - - name: Setup gems - run: bundle install --jobs 4 - - - name: Yarn install - run: yarn install --dev + bundler-cache: true - name: ESLint + if: ${{ always() }} run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts - name: Prettier + if: ${{ always() }} + shell: bash run: | yarn prettier -v - if [ -d "assets" ]; then \ - yarn prettier --list-different "assets/**/*.{scss,js,es6}" ; \ + if [ 0 -lt $(find assets -type f \( -name "*.scss" -or -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then + yarn prettier --list-different "assets/**/*.{scss,js,es6}" fi - if [ -d "test" ]; then \ - yarn prettier --list-different "test/**/*.{js,es6}" ; \ + if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then + yarn prettier --list-different "test/**/*.{js,es6}" fi - - name: Ember template lint - run: yarn ember-template-lint assets/javascripts - - name: Rubocop - run: bundle exec rubocop . \ No newline at end of file + if: ${{ always() }} + run: bundle exec rubocop . diff --git a/.github/workflows/plugin-metadata.yml b/.github/workflows/plugin-metadata.yml index c5e3caff..42069470 100644 --- a/.github/workflows/plugin-metadata.yml +++ b/.github/workflows/plugin-metadata.yml @@ -36,9 +36,9 @@ jobs: uses: actions/github-script@v5 with: script: | - const semver = require('semver'); - const { head_version, base_version } = process.env; + const semver = require('semver'); + const { head_version, base_version } = process.env; - if (semver.lte(head_version, base_version)) { - core.setFailed("Head version is equal to or lower than base version."); - } + if (semver.lte(head_version, base_version)) { + core.setFailed("Head version is equal to or lower than base version."); + } diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index aec4fa6c..f58f1b64 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -3,24 +3,25 @@ name: Plugin Tests on: push: branches: - - stable - - master - main + - stable pull_request: - schedule: - - cron: '0 */12 * * *' + +concurrency: + group: tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }} + cancel-in-progress: true jobs: build: name: ${{ matrix.build_type }} runs-on: ubuntu-latest - timeout-minutes: 60 + container: discourse/discourse_test:slim${{ startsWith(matrix.build_type, 'frontend') && '-browsers' || '' }} + timeout-minutes: 30 env: DISCOURSE_HOSTNAME: www.example.com RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072 RAILS_ENV: test - PGHOST: localhost PGUSER: discourse PGPASSWORD: discourse @@ -29,121 +30,107 @@ jobs: matrix: build_type: ["backend", "frontend"] - ruby: ["2.7"] - postgres: ["12"] - redis: ["6.x"] - - services: - postgres: - image: postgres:${{ matrix.postgres }} - ports: - - 5432:5432 - env: - POSTGRES_USER: discourse - POSTGRES_PASSWORD: discourse - options: >- - --mount type=tmpfs,destination=/var/lib/postgresql/data - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 steps: - - uses: haya14busa/action-cond@v1 - id: discourse_branch - with: - cond: ${{ github.base_ref == 'stable' }} - if_true: "stable" - if_false: "tests-passed" - - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: repository: discourse/discourse - ref: ${{ steps.discourse_branch.outputs.value }} fetch-depth: 1 - - name: Fetch Repo Name - id: repo-name - run: echo "::set-output name=value::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" - - name: Install plugin - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: - path: plugins/${{ steps.repo-name.outputs.value }} - ref: "${{ github.base_ref }}" + path: plugins/${{ github.event.repository.name }} fetch-depth: 1 - - name: Check spec existence - id: check_spec - uses: andstor/file-existence-action@v1 - with: - files: "plugins/${{ steps.repo-name.outputs.value }}/spec" - - - name: Check qunit existence - id: check_qunit - uses: andstor/file-existence-action@v1 - with: - files: "plugins/${{ steps.repo-name.outputs.value }}/test/javascripts" - - name: Setup Git run: | git config --global user.email "ci@ci.invalid" git config --global user.name "Discourse CI" - - name: Setup packages + - name: Start redis run: | - sudo apt-get update - sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead - wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh + redis-server /etc/redis/redis.conf & - - name: Update imagemagick - if: matrix.build_type == 'backend' + - name: Start Postgres run: | - wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick - chmod +x install-imagemagick - sudo ./install-imagemagick + chown -R postgres /var/run/postgresql + sudo -E -u postgres script/start_test_db.rb + sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';" - - name: Setup redis - uses: shogo82148/actions-setup-redis@v1 + - name: Bundler cache + uses: actions/cache@v3 with: - redis-version: ${{ matrix.redis }} + path: vendor/bundle + key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gem- - - name: Setup ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true + - name: Setup gems + run: | + gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock) + bundle config --local path vendor/bundle + bundle config --local deployment true + bundle config --local without development + bundle install --jobs 4 + bundle clean - name: Lint English locale if: matrix.build_type == 'backend' - run: bundle exec ruby script/i18n_lint.rb "plugins/${{ steps.repo-name.outputs.value }}/locales/{client,server}.en.yml" + run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml" - name: Get yarn cache directory id: yarn-cache-dir run: echo "::set-output name=dir::$(yarn cache dir)" - name: Yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir.outputs.dir }} - key: ${{ runner.os }}-${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | - ${{ runner.os }}-${{ matrix.os }}-yarn- + ${{ runner.os }}-yarn- - name: Yarn install - run: yarn install --dev + run: yarn install - - name: Migrate database + - name: Fetch app state cache + uses: actions/cache@v3 + id: app-cache + with: + path: tmp/app-cache + key: >- + ${{ hashFiles('.github/workflows/tests.yml') }}- + ${{ hashFiles('db/**/*', 'plugins/**/db/**/*') }}- + + - name: Restore database from cache + if: steps.app-cache.outputs.cache-hit == 'true' + run: psql -f tmp/app-cache/cache.sql postgres + + - name: Restore uploads from cache + if: steps.app-cache.outputs.cache-hit == 'true' + run: rm -rf public/uploads && cp -r tmp/app-cache/uploads public/uploads + + - name: Create and migrate database + if: steps.app-cache.outputs.cache-hit != 'true' run: | bin/rake db:create bin/rake db:migrate - - name: Plugin RSpec with Coverage - if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exists == 'true' - run: SIMPLECOV=1 bin/rake plugin:spec[${{ steps.repo-name.outputs.value }}] + - name: Dump database for cache + if: steps.app-cache.outputs.cache-hit != 'true' + run: mkdir -p tmp/app-cache && pg_dumpall > tmp/app-cache/cache.sql + + - name: Dump uploads for cache + if: steps.app-cache.outputs.cache-hit != 'true' + run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads + + - name: Plugin RSpec + if: matrix.build_type == 'backend' + run: bin/rake plugin:spec[${{ github.event.repository.name }}] - name: Plugin QUnit - if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exists == 'true' - run: bundle exec rake plugin:qunit['${{ steps.repo-name.outputs.value }}','1200000'] - timeout-minutes: 30 + if: matrix.build_type == 'frontend' + run: QUNIT_EMBER_CLI=1 bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000'] + timeout-minutes: 10 diff --git a/.gitignore b/.gitignore index 11ce0a3c..3da9ad01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ coverage/* !coverage/.last_run.json -gems/ +gems/* .bundle/ auto_generated .DS_Store node_modules/ +vendor/* diff --git a/.rubocop.yml b/.rubocop.yml index d46296cf..69fcfc56 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,8 @@ inherit_gem: rubocop-discourse: default.yml + +RSpec/ContextWording: + Enabled: false + +RSpec/DescribeClass: + Enabled: false diff --git a/.simplecov b/.simplecov new file mode 100644 index 00000000..c7b6143b --- /dev/null +++ b/.simplecov @@ -0,0 +1,7 @@ +# frozen_string_literal: true +plugin = "discourse-custom-wizard" + +SimpleCov.configure do + track_files "plugins/#{plugin}/**/*.rb" + add_filter { |src| !(src.filename =~ /(\/#{plugin}\/app\/|\/#{plugin}\/lib\/)/) } +end diff --git a/Gemfile.lock b/Gemfile.lock index 2416ce66..5ab57bb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,31 +2,32 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) - parallel (1.20.1) - parser (3.0.1.0) + json (2.6.2) + parallel (1.22.1) + parser (3.1.2.1) ast (~> 2.4.1) - rainbow (3.0.0) - regexp_parser (2.1.1) + rainbow (3.1.1) + regexp_parser (2.6.0) rexml (3.2.5) - rubocop (1.12.1) + rubocop (1.36.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.2.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.20.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.4.1) - parser (>= 2.7.1.5) - rubocop-discourse (2.4.1) + rubocop-ast (1.22.0) + parser (>= 3.1.1.0) + rubocop-discourse (3.0) rubocop (>= 1.1.0) rubocop-rspec (>= 2.0.0) - rubocop-rspec (2.2.0) - rubocop (~> 1.0) - rubocop-ast (>= 1.1.0) + rubocop-rspec (2.13.2) + rubocop (~> 1.33) ruby-progressbar (1.11.0) - unicode-display_width (2.0.0) + unicode-display_width (2.3.0) PLATFORMS ruby diff --git a/README.md b/README.md index 602709f5..0190f16e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ -# discourse-custom-wizard +# Discourse Custom Wizard Plugin -See further: https://thepavilion.io/c/knowledge/discourse/custom-wizard/118 +The Custom Wizard Plugin lets you make forms for your Discourse forum. Better user onboarding, structured posting, data enrichment, automated actions and much more for your community. + + + +## Install + +If you're not sure how to install a plugin in Discourse, please follow the [plugin installation guide](https://meta.discourse.org/t/install-a-plugin/19157) or contact your Discourse hosting provider. + +## Documentation + +[Read the full documentation here](https://discourse.pluginmanager.org/c/discourse-custom-wizard/documentation), or go directly to the relevant section + +- [Wizard Administration](https://discourse.pluginmanager.org/t/wizard-administration) +- [Wizard Settings](https://discourse.pluginmanager.org/t/wizard-settings) +- [Step Settings](https://discourse.pluginmanager.org/t/step-settings) +- [Field Settings](https://discourse.pluginmanager.org/t/field-settings) +- [Conditional Settings](https://discourse.pluginmanager.org/t/conditional-settings) +- [Field Interpolation](https://discourse.pluginmanager.org/t/field-interpolation) +- [Wizard Examples and Templates](https://discourse.pluginmanager.org/t/wizard-examples-and-templates) + +## Support + +- [Report a bug](https://discourse.pluginmanager.org/w/bug-report) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c799fa5a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +The security of Discourse plugins are premised on the security of [Discourse](https://github.com/discourse/discourse). Please first consider whether a security issue is best reported and handled by the Discourse team. You can view the Discourse security policy [here](https://github.com/discourse/discourse/security/policy). + +## Reporting a Vulnerability + +If you find a security vulnerability that is specific to this plugin, please report it to development@pavilion.tech. Security issues always take precedence over all other work. All commits specific to security are prefixed with SECURITY. diff --git a/controllers/custom_wizard/admin/admin.rb b/app/controllers/custom_wizard/admin/admin.rb similarity index 65% rename from controllers/custom_wizard/admin/admin.rb rename to app/controllers/custom_wizard/admin/admin.rb index c99954d6..867be56c 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/app/controllers/custom_wizard/admin/admin.rb @@ -3,6 +3,13 @@ class CustomWizard::AdminController < ::Admin::AdminController before_action :ensure_admin def index + subcription = CustomWizard::Subscription.new + render_json_dump( + subscribed: subcription.subscribed?, + subscription_type: subcription.type, + subscription_attributes: CustomWizard::Subscription.attributes, + subscription_client_installed: subcription.client_installed? + ) end private diff --git a/controllers/custom_wizard/admin/api.rb b/app/controllers/custom_wizard/admin/api.rb similarity index 93% rename from controllers/custom_wizard/admin/api.rb rename to app/controllers/custom_wizard/admin/api.rb index a913e9ca..e4ac31e9 100644 --- a/controllers/custom_wizard/admin/api.rb +++ b/app/controllers/custom_wizard/admin/api.rb @@ -20,6 +20,10 @@ class CustomWizard::AdminApiController < CustomWizard::AdminController raise Discourse::InvalidParameters, "An API with that name already exists: '#{current.title || current.name}'" end + unless subscription.includes?(:api, :all) + raise Discourse::InvalidParameters, "Your subscription doesn't include API features." + end + PluginStoreRow.transaction do CustomWizard::Api.set(api_params[:name], title: api_params[:title]) @@ -130,4 +134,8 @@ class CustomWizard::AdminApiController < CustomWizard::AdminController @auth_data ||= auth_data end + + def subscription + @subscription ||= CustomWizard::Subscription.new + end end diff --git a/controllers/custom_wizard/admin/custom_fields.rb b/app/controllers/custom_wizard/admin/custom_fields.rb similarity index 95% rename from controllers/custom_wizard/admin/custom_fields.rb rename to app/controllers/custom_wizard/admin/custom_fields.rb index c52759c9..111e9faf 100644 --- a/controllers/custom_wizard/admin/custom_fields.rb +++ b/app/controllers/custom_wizard/admin/custom_fields.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController def index - render_json_dump(custom_field_list) + render_json_dump( + custom_fields: custom_field_list + ) end def update diff --git a/app/controllers/custom_wizard/admin/logs.rb b/app/controllers/custom_wizard/admin/logs.rb new file mode 100644 index 00000000..7ca37bb2 --- /dev/null +++ b/app/controllers/custom_wizard/admin/logs.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +class CustomWizard::AdminLogsController < CustomWizard::AdminController + before_action :find_wizard, except: [:index] + + def index + render json: ActiveModel::ArraySerializer.new( + CustomWizard::Wizard.list(current_user), + each_serializer: CustomWizard::BasicWizardSerializer + ) + end + + def show + render_json_dump( + wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), + logs: ActiveModel::ArraySerializer.new( + log_list.logs, + each_serializer: CustomWizard::LogSerializer + ), + total: log_list.total + ) + end + + protected + + def log_list + @log_list ||= begin + list = CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i, params[:wizard_id]) + + if list.logs.any? && (usernames = list.logs.map(&:username)).present? + user_map = User.where(username: usernames) + .reduce({}) do |result, user| + result[user.username] = user + result + end + + list.logs.each do |log_item| + log_item.user = user_map[log_item.username] + end + end + + list + end + end +end diff --git a/controllers/custom_wizard/admin/manager.rb b/app/controllers/custom_wizard/admin/manager.rb similarity index 100% rename from controllers/custom_wizard/admin/manager.rb rename to app/controllers/custom_wizard/admin/manager.rb diff --git a/controllers/custom_wizard/admin/submissions.rb b/app/controllers/custom_wizard/admin/submissions.rb similarity index 68% rename from controllers/custom_wizard/admin/submissions.rb rename to app/controllers/custom_wizard/admin/submissions.rb index 4cb2a0e4..c3bf809f 100644 --- a/controllers/custom_wizard/admin/submissions.rb +++ b/app/controllers/custom_wizard/admin/submissions.rb @@ -13,12 +13,16 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController def show render_json_dump( wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), - submissions: ActiveModel::ArraySerializer.new(ordered_submissions, each_serializer: CustomWizard::SubmissionSerializer) + submissions: ActiveModel::ArraySerializer.new( + submission_list.submissions, + each_serializer: CustomWizard::SubmissionSerializer + ), + total: submission_list.total ) end def download - send_data ordered_submissions.to_json, + send_data submission_list.submissions.to_json, filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json", content_type: "application/json", disposition: "attachment" @@ -26,7 +30,7 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController protected - def ordered_submissions - CustomWizard::Submission.list(@wizard, order_by: 'id') + def submission_list + CustomWizard::Submission.list(@wizard, page: params[:page].to_i) end end diff --git a/controllers/custom_wizard/admin/wizard.rb b/app/controllers/custom_wizard/admin/wizard.rb similarity index 95% rename from controllers/custom_wizard/admin/wizard.rb rename to app/controllers/custom_wizard/admin/wizard.rb index 0a59e02b..08e7b6d0 100644 --- a/controllers/custom_wizard/admin/wizard.rb +++ b/app/controllers/custom_wizard/admin/wizard.rb @@ -88,6 +88,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController :title, :key, :banner, + :banner_upload_id, :raw_description, :required_data_message, :force_final, @@ -99,6 +100,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController :index, :label, :image, + :image_upload_id, :description, :required, :key, @@ -112,6 +114,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController :property, :preview_template, :placeholder, + :can_create_tag, prefill: mapped_params, content: mapped_params, condition: mapped_params, @@ -161,7 +164,9 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController mentionable_level: mapped_params, messageable_level: mapped_params, visibility_level: mapped_params, - members_visibility_level: mapped_params + members_visibility_level: mapped_params, + add_event: mapped_params, + add_location: mapped_params ] ) end diff --git a/controllers/custom_wizard/realtime_validations.rb b/app/controllers/custom_wizard/realtime_validations.rb similarity index 100% rename from controllers/custom_wizard/realtime_validations.rb rename to app/controllers/custom_wizard/realtime_validations.rb diff --git a/controllers/custom_wizard/steps.rb b/app/controllers/custom_wizard/steps.rb similarity index 100% rename from controllers/custom_wizard/steps.rb rename to app/controllers/custom_wizard/steps.rb diff --git a/app/controllers/custom_wizard/wizard.rb b/app/controllers/custom_wizard/wizard.rb new file mode 100644 index 00000000..7aafdd3b --- /dev/null +++ b/app/controllers/custom_wizard/wizard.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +class CustomWizard::WizardController < ::ApplicationController + before_action :ensure_plugin_enabled + before_action :ensure_logged_in, only: [:skip] + + def show + if wizard.present? + render json: CustomWizard::WizardSerializer.new(wizard, scope: guardian, root: false).as_json, status: 200 + else + render json: { error: I18n.t('wizard.none') } + end + end + + def skip + params.require(:wizard_id) + + if wizard.required && !wizard.completed? && wizard.permitted? + return render json: { error: I18n.t('wizard.no_skip') } + end + + result = { success: 'OK' } + + if current_user && wizard.can_access? + if redirect_to = wizard.current_submission&.redirect_to + result.merge!(redirect_to: redirect_to) + end + + wizard.cleanup_on_skip! + end + + render json: result + end + + protected + + def wizard + @wizard ||= begin + builder = CustomWizard::Builder.new(params[:wizard_id].underscore, current_user) + return nil unless builder.present? + opts = {} + opts[:reset] = params[:reset] + builder.build(opts, params) + end + end + + private + + def ensure_plugin_enabled + unless SiteSetting.custom_wizard_enabled + redirect_to path("/") + end + end +end diff --git a/jobs/refresh_api_access_token.rb b/app/jobs/regular/refresh_api_access_token.rb similarity index 100% rename from jobs/refresh_api_access_token.rb rename to app/jobs/regular/refresh_api_access_token.rb diff --git a/jobs/set_after_time_wizard.rb b/app/jobs/regular/set_after_time_wizard.rb similarity index 100% rename from jobs/set_after_time_wizard.rb rename to app/jobs/regular/set_after_time_wizard.rb diff --git a/serializers/custom_wizard/api/authorization_serializer.rb b/app/serializers/custom_wizard/api/authorization_serializer.rb similarity index 100% rename from serializers/custom_wizard/api/authorization_serializer.rb rename to app/serializers/custom_wizard/api/authorization_serializer.rb diff --git a/serializers/custom_wizard/api/basic_endpoint_serializer.rb b/app/serializers/custom_wizard/api/basic_endpoint_serializer.rb similarity index 100% rename from serializers/custom_wizard/api/basic_endpoint_serializer.rb rename to app/serializers/custom_wizard/api/basic_endpoint_serializer.rb diff --git a/serializers/custom_wizard/api/endpoint_serializer.rb b/app/serializers/custom_wizard/api/endpoint_serializer.rb similarity index 100% rename from serializers/custom_wizard/api/endpoint_serializer.rb rename to app/serializers/custom_wizard/api/endpoint_serializer.rb diff --git a/serializers/custom_wizard/api/log_serializer.rb b/app/serializers/custom_wizard/api/log_serializer.rb similarity index 100% rename from serializers/custom_wizard/api/log_serializer.rb rename to app/serializers/custom_wizard/api/log_serializer.rb diff --git a/serializers/custom_wizard/api_serializer.rb b/app/serializers/custom_wizard/api_serializer.rb similarity index 100% rename from serializers/custom_wizard/api_serializer.rb rename to app/serializers/custom_wizard/api_serializer.rb diff --git a/serializers/custom_wizard/basic_api_serializer.rb b/app/serializers/custom_wizard/basic_api_serializer.rb similarity index 100% rename from serializers/custom_wizard/basic_api_serializer.rb rename to app/serializers/custom_wizard/basic_api_serializer.rb diff --git a/serializers/custom_wizard/basic_wizard_serializer.rb b/app/serializers/custom_wizard/basic_wizard_serializer.rb similarity index 100% rename from serializers/custom_wizard/basic_wizard_serializer.rb rename to app/serializers/custom_wizard/basic_wizard_serializer.rb diff --git a/serializers/custom_wizard/custom_field_serializer.rb b/app/serializers/custom_wizard/custom_field_serializer.rb similarity index 100% rename from serializers/custom_wizard/custom_field_serializer.rb rename to app/serializers/custom_wizard/custom_field_serializer.rb diff --git a/app/serializers/custom_wizard/log_serializer.rb b/app/serializers/custom_wizard/log_serializer.rb new file mode 100644 index 00000000..56c5fd8f --- /dev/null +++ b/app/serializers/custom_wizard/log_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CustomWizard::LogSerializer < ApplicationSerializer + attributes :date, + :action, + :username, + :message + + has_one :user, serializer: ::BasicUserSerializer, embed: :objects +end diff --git a/serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb b/app/serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb similarity index 100% rename from serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb rename to app/serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb diff --git a/app/serializers/custom_wizard/submission_serializer.rb b/app/serializers/custom_wizard/submission_serializer.rb new file mode 100644 index 00000000..732d6743 --- /dev/null +++ b/app/serializers/custom_wizard/submission_serializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +class CustomWizard::SubmissionSerializer < ApplicationSerializer + attributes :id, + :fields, + :submitted_at + + has_one :user, serializer: ::BasicUserSerializer, embed: :objects + + def include_user? + object.user.present? + end + + def fields + @fields ||= begin + result = {} + + object.wizard.template['steps'].each do |step| + step['fields'].each do |field| + if value = object.fields[field['id']] + result[field['id']] = { + value: value, + type: field['type'], + label: field['label'] + } + end + end + end + + result + end + end +end diff --git a/serializers/custom_wizard/wizard_field_serializer.rb b/app/serializers/custom_wizard/wizard_field_serializer.rb similarity index 77% rename from serializers/custom_wizard/wizard_field_serializer.rb rename to app/serializers/custom_wizard/wizard_field_serializer.rb index 70784f7f..56e86cc8 100644 --- a/serializers/custom_wizard/wizard_field_serializer.rb +++ b/app/serializers/custom_wizard/wizard_field_serializer.rb @@ -17,6 +17,7 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer :property, :content, :tag_groups, + :can_create_tag, :validations, :max_length, :char_counter, @@ -42,13 +43,8 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer object.value end - def i18n_key - @i18n_key ||= "wizard.step.#{object.step.id}.fields.#{object.id}".underscore - end - def label - return object.label if object.label.present? - I18n.t("#{object.key || i18n_key}.label", default: '') + I18n.t("#{i18n_key}.label", default: object.label, base_url: Discourse.base_url) end def include_label? @@ -56,14 +52,21 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer end def description - return object.description if object.description.present? - I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url) + I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url) end def include_description? description.present? end + def placeholder + I18n.t("#{i18n_key}.placeholder", default: object.placeholder) + end + + def include_placeholder? + placeholder.present? + end + def image object.image end @@ -72,15 +75,6 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer object.image.present? end - def placeholder - return object.placeholder if object.placeholder.present? - I18n.t("#{object.key || i18n_key}.placeholder", default: '') - end - - def include_placeholder? - placeholder.present? - end - def file_types object.file_types end @@ -105,6 +99,10 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer object.tag_groups end + def can_create_tag + object.can_create_tag + end + def validations validations = {} object.validations&.each do |type, props| @@ -127,4 +125,14 @@ class CustomWizard::FieldSerializer < ::ApplicationSerializer def preview_template object.preview_template end + + protected + + def i18n_key + @i18n_key ||= "#{object.step.wizard.id}.#{object.step.id}.#{object.id}".underscore + end + + def subscribed? + @subscribed ||= CustomWizard::Subscription.subscribed? + end end diff --git a/serializers/custom_wizard/wizard_serializer.rb b/app/serializers/custom_wizard/wizard_serializer.rb similarity index 61% rename from serializers/custom_wizard/wizard_serializer.rb rename to app/serializers/custom_wizard/wizard_serializer.rb index 708b962d..9741d7af 100644 --- a/serializers/custom_wizard/wizard_serializer.rb +++ b/app/serializers/custom_wizard/wizard_serializer.rb @@ -9,13 +9,10 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer :completed, :required, :permitted, - :uncategorized_category_id, - :categories, :resume_on_revisit has_many :steps, serializer: ::CustomWizard::StepSerializer, embed: :objects has_one :user, serializer: ::BasicUserSerializer, embed: :objects - has_many :groups, serializer: ::BasicGroupSerializer, embed: :objects def completed object.completed? @@ -46,30 +43,4 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer def include_steps? !include_completed? end - - def include_categories? - object.needs_categories - end - - def include_groups? - object.needs_groups - end - - def uncategorized_category_id - SiteSetting.uncategorized_category_id - end - - def include_uncategorized_category_id? - object.needs_categories - end - - def categories - object.categories.map do |category| - if category.respond_to?(:to_h) - category.to_h - else - ::BasicCategorySerializer.new(category).as_json - end - end - end end diff --git a/serializers/custom_wizard/wizard_step_serializer.rb b/app/serializers/custom_wizard/wizard_step_serializer.rb similarity index 76% rename from serializers/custom_wizard/wizard_step_serializer.rb rename to app/serializers/custom_wizard/wizard_step_serializer.rb index 85f527bb..a2a314a4 100644 --- a/serializers/custom_wizard/wizard_step_serializer.rb +++ b/app/serializers/custom_wizard/wizard_step_serializer.rb @@ -39,13 +39,8 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer object.previous.present? end - def i18n_key - @i18n_key ||= "wizard.step.#{object.id}".underscore - end - def title - return PrettyText.cook(object.title) if object.title - PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: '')) + I18n.t("#{i18n_key}.title", default: object.title, base_url: Discourse.base_url) end def include_title? @@ -53,8 +48,7 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer end def description - return object.description if object.description - PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)) + I18n.t("#{i18n_key}.description", default: object.description, base_url: Discourse.base_url) end def include_description? @@ -80,4 +74,10 @@ class CustomWizard::StepSerializer < ::ApplicationSerializer def final object.final? end + + protected + + def i18n_key + @i18n_key ||= "#{object.wizard.id}.#{object.id}".underscore + end end diff --git a/app/views/layouts/qunit.html.erb b/app/views/layouts/qunit.html.erb new file mode 100644 index 00000000..c2f5fb5e --- /dev/null +++ b/app/views/layouts/qunit.html.erb @@ -0,0 +1,28 @@ + + + + Custom Wizard QUnit Test Runner + <%= discourse_stylesheet_link_tag(:test_helper, theme_id: nil) %> + <%= discourse_stylesheet_link_tag :wizard, theme_id: nil %> + <%= discourse_stylesheet_link_tag :wizard_custom %> + <%= preload_script "locales/en" %> + <%= preload_script "ember_jquery" %> + <%= preload_script "wizard-vendor" %> + <%= preload_script "wizard-custom" %> + <%= preload_script "wizard-raw-templates" %> + <%= preload_script "wizard-plugin" %> + <%= preload_script "pretty-text-bundle" %> + <%= preload_script "wizard-qunit" %> + <%= csrf_meta_tags %> + + + + <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %> + + + + +
+
+ + diff --git a/assets/javascripts/discourse/components/custom-field-input.js.es6 b/assets/javascripts/discourse/components/custom-field-input.js.es6 index e49c6f1d..5d2d6c3b 100644 --- a/assets/javascripts/discourse/components/custom-field-input.js.es6 +++ b/assets/javascripts/discourse/components/custom-field-input.js.es6 @@ -3,27 +3,12 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { alias, equal, or } from "@ember/object/computed"; import I18n from "I18n"; -const generateContent = function (array, type) { - return array.map((key) => ({ - id: key, - name: I18n.t(`admin.wizard.custom_field.${type}.${key}`), - })); -}; - export default Component.extend({ tagName: "tr", topicSerializers: ["topic_view", "topic_list_item"], postSerializers: ["post"], groupSerializers: ["basic_group"], categorySerializers: ["basic_category"], - klassContent: generateContent( - ["topic", "post", "group", "category"], - "klass" - ), - typeContent: generateContent( - ["string", "boolean", "integer", "json"], - "type" - ), showInputs: or("field.new", "field.edit"), classNames: ["custom-field-input"], loading: or("saving", "destroying"), @@ -40,9 +25,13 @@ export default Component.extend({ const serializers = this.get(`${klass}Serializers`); if (serializers) { - return generateContent(serializers, "serializers"); - } else { - return []; + return serializers.reduce((result, key) => { + result.push({ + id: key, + name: I18n.t(`admin.wizard.custom_field.serializers.${key}`), + }); + return result; + }, []); } }, diff --git a/assets/javascripts/wizard/components/custom-user-selector.js.es6 b/assets/javascripts/discourse/components/custom-user-selector.js.es6 similarity index 91% rename from assets/javascripts/wizard/components/custom-user-selector.js.es6 rename to assets/javascripts/discourse/components/custom-user-selector.js.es6 index b2f08ede..3bb1fb3d 100644 --- a/assets/javascripts/wizard/components/custom-user-selector.js.es6 +++ b/assets/javascripts/discourse/components/custom-user-selector.js.es6 @@ -3,10 +3,11 @@ import { observes, } from "discourse-common/utils/decorators"; import { renderAvatar } from "discourse/helpers/user-avatar"; -import userSearch from "../lib/user-search"; -import WizardI18n from "../lib/wizard-i18n"; +import userSearch from "discourse/lib/user-search"; +import I18n from "I18n"; import Handlebars from "handlebars"; import { isEmpty } from "@ember/utils"; +import TextField from "@ember/component/text-field"; const template = function (params) { const options = params.options; @@ -31,7 +32,7 @@ const template = function (params) { return new Handlebars.SafeString(html).string; }; -export default Ember.TextField.extend({ +export default TextField.extend({ attributeBindings: ["autofocus", "maxLength"], autocorrect: false, autocapitalize: false, @@ -40,7 +41,7 @@ export default Ember.TextField.extend({ @computed("placeholderKey") placeholder(placeholderKey) { - return placeholderKey ? WizardI18n(placeholderKey) : ""; + return placeholderKey ? I18n.t(placeholderKey) : ""; }, @observes("usernames") @@ -55,7 +56,6 @@ export default Ember.TextField.extend({ let self = this, selected = [], groups = [], - currentUser = this.currentUser, includeMentionableGroups = this.get("includeMentionableGroups") === "true", includeMessageableGroups = @@ -66,13 +66,8 @@ export default Ember.TextField.extend({ function excludedUsernames() { // hack works around some issues with allowAny eventing const usernames = self.get("single") ? [] : selected; - - if (currentUser && self.get("excludeCurrentUser")) { - return usernames.concat([currentUser.get("username")]); - } return usernames; } - $(this.element) .val(this.get("usernames")) .autocomplete({ @@ -84,7 +79,6 @@ export default Ember.TextField.extend({ dataSource(term) { const termRegex = /[^a-zA-Z0-9_\-\.@\+]/; - let results = userSearch({ term: term.replace(termRegex, ""), topicId: self.get("topicId"), diff --git a/assets/javascripts/wizard/components/wizard-category-selector.js.es6 b/assets/javascripts/discourse/components/custom-wizard-category-selector.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-category-selector.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-category-selector.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 similarity index 81% rename from assets/javascripts/wizard/components/wizard-composer-editor.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 index faada1f4..5e2ef424 100644 --- a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 @@ -1,5 +1,6 @@ import ComposerEditor from "discourse/components/composer-editor"; import { + bind, default as discourseComputed, on, } from "discourse-common/utils/decorators"; @@ -8,10 +9,12 @@ import { scheduleOnce } from "@ember/runloop"; import { caretPosition, inCodeBlock } from "discourse/lib/utilities"; import highlightSyntax from "discourse/lib/highlight-syntax"; import { alias } from "@ember/object/computed"; -import Site from "../models/site"; +import Site from "discourse/models/site"; import { uploadIcon } from "discourse/lib/uploads"; import { dasherize } from "@ember/string"; +const IMAGE_MARKDOWN_REGEX = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g; + export default ComposerEditor.extend({ classNameBindings: ["fieldClass"], allowUpload: true, @@ -24,8 +27,8 @@ export default ComposerEditor.extend({ lastValidatedAt: "lastValidatedAt", popupMenuOptions: [], draftStatus: "null", - replyPlaceholder: alias("field.placeholder"), - uploadingFieldId: null, + replyPlaceholder: alias("field.translatedPlaceholder"), + wizardEventFieldId: null, @on("didInsertElement") _composerEditorInit() { @@ -34,7 +37,7 @@ export default ComposerEditor.extend({ if (this.siteSettings.enable_mentions) { $input.autocomplete({ template: findRawTemplate("user-selector-autocomplete"), - dataSource: (term) => this.userSearchTerm.call(this, term), + dataSource: (term) => this._userSearchTerm.call(this, term), key: "@", transformComplete: (v) => v.username || v.name, afterComplete: (value) => { @@ -76,7 +79,6 @@ export default ComposerEditor.extend({ const wizardEventNames = ["insert-text", "replace-text"]; const eventPrefix = this.eventPrefix; - const session = this.get("session"); this.appEvents.reopen({ trigger(name, ...args) { let eventParts = name.split(":"); @@ -87,26 +89,8 @@ export default ComposerEditor.extend({ currentEventPrefix !== "wizard-editor" && wizardEventNames.some((wen) => wen === currentEventName) ) { - let wizardName = name.replace(eventPrefix, "wizard-editor"); - if (currentEventName === "insert-text") { - args = { - text: args[0], - }; - } - if (currentEventName === "replace-text") { - args = { - oldVal: args[0], - newVal: args[1], - }; - } - let wizardArgs = Object.assign( - {}, - { - fieldId: session.get("uploadingFieldId"), - }, - args - ); - return this._super(wizardName, wizardArgs); + let wizardEventName = name.replace(eventPrefix, "wizard-editor"); + return this._super(wizardEventName, ...args); } else { return this._super(name, ...args); } @@ -138,11 +122,34 @@ export default ComposerEditor.extend({ } }, + @bind + _handleImageDeleteButtonClick(event) { + if (!event.target.classList.contains("delete-image-button")) { + return; + } + + const index = parseInt( + event.target.closest(".button-wrapper").dataset.imageIndex, + 10 + ); + const matchingPlaceholder = this.get("composer.reply").match( + IMAGE_MARKDOWN_REGEX + ); + + this.session.set("wizardEventFieldId", this.field.id); + this.appEvents.trigger( + "composer:replace-text", + matchingPlaceholder[index], + "", + { regex: IMAGE_MARKDOWN_REGEX, index } + ); + }, + actions: { extraButtons(toolbar) { const component = this; - if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { + if (this.allowUpload && this.uploadIcon) { toolbar.addButton({ id: "upload", group: "insertions", @@ -187,14 +194,14 @@ export default ComposerEditor.extend({ } }, - previewUpdated($preview) { - highlightSyntax($preview[0], this.siteSettings, this.session); + previewUpdated(preview) { + highlightSyntax(preview, this.siteSettings, this.session); if (this.siteSettings.mentionables_enabled) { const { linkSeenMentionableItems } = requirejs( "discourse/plugins/discourse-mentionables/discourse/lib/mentionable-items-preview-styling" ); - linkSeenMentionableItems($preview, this.siteSettings); + linkSeenMentionableItems(preview, this.siteSettings); } this._super(...arguments); }, @@ -213,7 +220,7 @@ export default ComposerEditor.extend({ }, showUploadModal() { - this.session.set("uploadingFieldId", this.field.id); + this.session.set("wizardEventFieldId", this.field.id); document.getElementById(this.fileUploadElementId).click(); }, }, diff --git a/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 b/assets/javascripts/discourse/components/custom-wizard-composer-hyperlink.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-composer-hyperlink.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-date-input.js.es6 b/assets/javascripts/discourse/components/custom-wizard-date-input.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-date-input.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-date-input.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-date-time-input.js.es6 b/assets/javascripts/discourse/components/custom-wizard-date-time-input.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-date-time-input.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-date-time-input.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-field-category.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-category.js.es6 similarity index 92% rename from assets/javascripts/wizard/components/wizard-field-category.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-category.js.es6 index a7452214..65e4a1c7 100644 --- a/assets/javascripts/wizard/components/wizard-field-category.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-field-category.js.es6 @@ -1,7 +1,8 @@ import { observes } from "discourse-common/utils/decorators"; import Category from "discourse/models/category"; +import Component from "@ember/component"; -export default Ember.Component.extend({ +export default Component.extend({ didInsertElement() { const property = this.field.property || "id"; const value = this.field.value; diff --git a/assets/javascripts/discourse/components/custom-wizard-field-checkbox.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-checkbox.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-checkbox.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/wizard/components/wizard-field-composer-preview.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-composer-preview.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-field-composer-preview.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-composer-preview.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-composer.js.es6 similarity index 92% rename from assets/javascripts/wizard/components/wizard-field-composer.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-composer.js.es6 index 8b9ecb82..1a25344c 100644 --- a/assets/javascripts/wizard/components/wizard-field-composer.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-field-composer.js.es6 @@ -3,8 +3,9 @@ import { observes, } from "discourse-common/utils/decorators"; import EmberObject from "@ember/object"; +import Component from "@ember/component"; -export default Ember.Component.extend({ +export default Component.extend({ showPreview: false, classNameBindings: [ ":wizard-field-composer", diff --git a/assets/javascripts/wizard/components/wizard-field-date-time.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-date-time.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-field-date-time.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-date-time.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-field-date.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-date.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-field-date.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-date.js.es6 diff --git a/assets/javascripts/discourse/components/custom-wizard-field-dropdown.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-dropdown.js.es6 new file mode 100644 index 00000000..659b8f29 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-dropdown.js.es6 @@ -0,0 +1,13 @@ +import Component from "@ember/component"; + +export default Component.extend({ + keyPress(e) { + e.stopPropagation(); + }, + + actions: { + onChangeValue(value) { + this.set("field.value", value); + }, + }, +}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-group.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-group.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-group.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-number.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-number.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-number.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-tag.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-tag.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-tag.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-text.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-text.js.es6 new file mode 100644 index 00000000..3e49cb35 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-text.js.es6 @@ -0,0 +1,7 @@ +import Component from "@ember/component"; + +export default Component.extend({ + keyPress(e) { + e.stopPropagation(); + }, +}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-textarea.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-textarea.js.es6 new file mode 100644 index 00000000..3e49cb35 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-textarea.js.es6 @@ -0,0 +1,7 @@ +import Component from "@ember/component"; + +export default Component.extend({ + keyPress(e) { + e.stopPropagation(); + }, +}); diff --git a/assets/javascripts/wizard/components/wizard-field-time.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-time.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-field-time.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-time.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-field-upload.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-upload.js.es6 similarity index 88% rename from assets/javascripts/wizard/components/wizard-field-upload.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-field-upload.js.es6 index 876cdf0e..eb5d318b 100644 --- a/assets/javascripts/wizard/components/wizard-field-upload.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-field-upload.js.es6 @@ -9,6 +9,9 @@ export default Component.extend(UppyUploadMixin, { type: computed(function () { return `wizard_${this.field.id}`; }), + id: computed(function () { + return `wizard_field_upload_${this.field.id}`; + }), isImage: computed("field.value.extension", function () { return ( this.field.value && diff --git a/assets/javascripts/discourse/components/custom-wizard-field-url.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-url.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-url.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field-user-selector.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field-user-selector.js.es6 new file mode 100644 index 00000000..87d5ddb0 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field-user-selector.js.es6 @@ -0,0 +1,3 @@ +import Component from "@ember/component"; + +export default Component.extend({}); diff --git a/assets/javascripts/discourse/components/custom-wizard-field.js.es6 b/assets/javascripts/discourse/components/custom-wizard-field.js.es6 new file mode 100644 index 00000000..d706ecd8 --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-field.js.es6 @@ -0,0 +1,41 @@ +import Component from "@ember/component"; +import { dasherize } from "@ember/string"; +import discourseComputed from "discourse-common/utils/decorators"; +import { cookAsync } from "discourse/lib/text"; + +export default Component.extend({ + classNameBindings: [ + ":wizard-field", + "typeClasses", + "field.invalid", + "field.id", + ], + + didReceiveAttrs() { + this._super(...arguments); + + cookAsync(this.field.translatedDescription).then((cookedDescription) => { + this.set("cookedDescription", cookedDescription); + }); + }, + + @discourseComputed("field.type", "field.id") + typeClasses: (type, id) => + `${dasherize(type)}-field ${dasherize(type)}-${dasherize(id)}`, + + @discourseComputed("field.id") + fieldClass: (id) => `field-${dasherize(id)} wizard-focusable`, + + @discourseComputed("field.type", "field.id") + inputComponentName(type, id) { + if (["text_only"].includes(type)) { + return false; + } + return dasherize(type === "component" ? id : `custom-wizard-field-${type}`); + }, + + @discourseComputed("field.type") + textType(fieldType) { + return ["text", "textarea"].includes(fieldType); + }, +}); diff --git a/assets/javascripts/wizard/components/wizard-group-selector.js.es6 b/assets/javascripts/discourse/components/custom-wizard-group-selector.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-group-selector.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-group-selector.js.es6 diff --git a/assets/javascripts/discourse/components/custom-wizard-no-access.js.es6 b/assets/javascripts/discourse/components/custom-wizard-no-access.js.es6 new file mode 100644 index 00000000..b3b2e26c --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-no-access.js.es6 @@ -0,0 +1,29 @@ +import CustomWizard from "../models/custom-wizard"; +import discourseComputed from "discourse-common/utils/decorators"; +import Component from "@ember/component"; +import { dasherize } from "@ember/string"; +import getURL from "discourse-common/lib/get-url"; + +export default Component.extend({ + classNameBindings: [":wizard-no-access", "reasonClass"], + + @discourseComputed("reason") + reasonClass(reason) { + return dasherize(reason); + }, + + @discourseComputed + siteName() { + return this.siteSettings.title || ""; + }, + + actions: { + skip() { + if (this.currentUser) { + CustomWizard.skip(this.get("wizardId")); + } else { + window.location = getURL("/"); + } + }, + }, +}); diff --git a/assets/javascripts/wizard/components/wizard-similar-topics.js.es6 b/assets/javascripts/discourse/components/custom-wizard-similar-topics.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-similar-topics.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-similar-topics.js.es6 diff --git a/assets/javascripts/discourse/components/custom-wizard-step-form.js.es6 b/assets/javascripts/discourse/components/custom-wizard-step-form.js.es6 new file mode 100644 index 00000000..73406b4f --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-step-form.js.es6 @@ -0,0 +1,9 @@ +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Component.extend({ + classNameBindings: [":wizard-step-form", "customStepClass"], + + @discourseComputed("step.id") + customStepClass: (stepId) => `wizard-step-${stepId}`, +}); diff --git a/assets/javascripts/discourse/components/custom-wizard-step.js.es6 b/assets/javascripts/discourse/components/custom-wizard-step.js.es6 new file mode 100644 index 00000000..b98db1ab --- /dev/null +++ b/assets/javascripts/discourse/components/custom-wizard-step.js.es6 @@ -0,0 +1,233 @@ +import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import Component from "@ember/component"; +import I18n from "I18n"; +import getUrl from "discourse-common/lib/get-url"; +import { htmlSafe } from "@ember/template"; +import { schedule } from "@ember/runloop"; +import { cookAsync } from "discourse/lib/text"; +import CustomWizard, { + updateCachedWizard, +} from "discourse/plugins/discourse-custom-wizard/discourse/models/custom-wizard"; +import { alias, not } from "@ember/object/computed"; + +const alreadyWarned = {}; + +export default Component.extend({ + classNameBindings: [":wizard-step", "step.id"], + saving: null, + + init() { + this._super(...arguments); + this.set("stylingDropdown", {}); + }, + + didReceiveAttrs() { + this._super(...arguments); + + cookAsync(this.step.translatedTitle).then((cookedTitle) => { + this.set("cookedTitle", cookedTitle); + }); + cookAsync(this.step.translatedDescription).then((cookedDescription) => { + this.set("cookedDescription", cookedDescription); + }); + }, + + didInsertElement() { + this._super(...arguments); + this.autoFocus(); + }, + + @discourseComputed("step.index", "wizard.required") + showQuitButton: (index, required) => index === 0 && !required, + + showNextButton: not("step.final"), + showDoneButton: alias("step.final"), + + @discourseComputed( + "step.index", + "step.displayIndex", + "wizard.totalSteps", + "wizard.completed" + ) + showFinishButton: (index, displayIndex, total, completed) => { + return index !== 0 && displayIndex !== total && completed; + }, + + @discourseComputed("step.index") + showBackButton: (index) => index > 0, + + @discourseComputed("step.banner") + bannerImage(src) { + if (!src) { + return; + } + return getUrl(src); + }, + + @discourseComputed("step.id") + bannerAndDescriptionClass(id) { + return `wizard-banner-and-description wizard-banner-and-description-${id}`; + }, + + @discourseComputed("step.fields.[]") + primaryButtonIndex(fields) { + return fields.length + 1; + }, + + @discourseComputed("step.fields.[]") + secondaryButtonIndex(fields) { + return fields.length + 2; + }, + + @observes("step.id") + _stepChanged() { + this.set("saving", false); + this.autoFocus(); + }, + + @observes("step.message") + _handleMessage: function () { + const message = this.get("step.message"); + this.showMessage(message); + }, + + @discourseComputed("step.index", "wizard.totalSteps") + barStyle(displayIndex, totalSteps) { + let ratio = parseFloat(displayIndex) / parseFloat(totalSteps - 1); + if (ratio < 0) { + ratio = 0; + } + if (ratio > 1) { + ratio = 1; + } + + return htmlSafe(`width: ${ratio * 200}px`); + }, + + @discourseComputed("step.fields") + includeSidebar(fields) { + return !!fields.findBy("show_in_sidebar"); + }, + + autoFocus() { + schedule("afterRender", () => { + const $invalid = $( + ".wizard-field.invalid:nth-of-type(1) .wizard-focusable" + ); + + if ($invalid.length) { + return $invalid.focus(); + } + + $(".wizard-focusable:first").focus(); + }); + }, + + animateInvalidFields() { + schedule("afterRender", () => { + let $element = $( + ".invalid input[type=text],.invalid textarea,.invalid input[type=checkbox],.invalid .select-kit" + ); + + if ($element.length) { + $([document.documentElement, document.body]).animate( + { + scrollTop: $element.offset().top - 200, + }, + 400 + ); + } + }); + }, + + advance() { + this.set("saving", true); + this.get("step") + .save() + .then((response) => { + updateCachedWizard(CustomWizard.build(response["wizard"])); + + if (response["final"]) { + CustomWizard.finished(response); + } else { + this.goNext(response); + } + }) + .catch(() => this.animateInvalidFields()) + .finally(() => this.set("saving", false)); + }, + + actions: { + quit() { + this.get("wizard").skip(); + }, + + done() { + this.send("nextStep"); + }, + + showMessage(message) { + this.sendAction(message); + }, + + stylingDropdownChanged(id, value) { + this.set("stylingDropdown", { id, value }); + }, + + exitEarly() { + const step = this.step; + step.validate(); + + if (step.get("valid")) { + this.set("saving", true); + + step + .save() + .then(() => this.send("quit")) + .finally(() => this.set("saving", false)); + } else { + this.autoFocus(); + } + }, + + backStep() { + if (this.saving) { + return; + } + + this.goBack(); + }, + + nextStep() { + if (this.saving) { + return; + } + + const step = this.step; + const result = step.validate(); + + if (result.warnings.length) { + const unwarned = result.warnings.filter((w) => !alreadyWarned[w]); + if (unwarned.length) { + unwarned.forEach((w) => (alreadyWarned[w] = true)); + return window.bootbox.confirm( + unwarned.map((w) => I18n.t(`wizard.${w}`)).join("\n"), + I18n.t("no_value"), + I18n.t("yes_value"), + (confirmed) => { + if (confirmed) { + this.advance(); + } + } + ); + } + } + + if (step.get("valid")) { + this.advance(); + } else { + this.autoFocus(); + } + }, + }, +}); diff --git a/assets/javascripts/wizard/components/wizard-tag-chooser.js.es6 b/assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-tag-chooser.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-tag-selector.js.es6 b/assets/javascripts/discourse/components/custom-wizard-tag-selector.js.es6 similarity index 100% rename from assets/javascripts/wizard/components/wizard-tag-selector.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-tag-selector.js.es6 diff --git a/assets/javascripts/wizard/components/wizard-text-field.js.es6 b/assets/javascripts/discourse/components/custom-wizard-text-field.js.es6 similarity index 71% rename from assets/javascripts/wizard/components/wizard-text-field.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-text-field.js.es6 index c522eb2c..e8c22e93 100644 --- a/assets/javascripts/wizard/components/wizard-text-field.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-text-field.js.es6 @@ -1,10 +1,9 @@ -/* eslint no-undef: 0*/ - import computed from "discourse-common/utils/decorators"; import { isLTR, isRTL, siteDir } from "discourse/lib/text-direction"; -import WizardI18n from "../lib/wizard-i18n"; +import I18n from "I18n"; +import TextField from "@ember/component/text-field"; -export default Ember.TextField.extend({ +export default TextField.extend({ attributeBindings: [ "autocorrect", "autocapitalize", @@ -15,7 +14,7 @@ export default Ember.TextField.extend({ @computed dir() { - if (Wizard.SiteSettings.support_mixed_text_direction) { + if (this.siteSettings.support_mixed_text_direction) { let val = this.value; if (val) { return isRTL(val) ? "rtl" : "ltr"; @@ -26,7 +25,7 @@ export default Ember.TextField.extend({ }, keyUp() { - if (Wizard.SiteSettings.support_mixed_text_direction) { + if (this.siteSettings.support_mixed_text_direction) { let val = this.value; if (isRTL(val)) { this.set("dir", "rtl"); @@ -40,6 +39,6 @@ export default Ember.TextField.extend({ @computed("placeholderKey") placeholder(placeholderKey) { - return placeholderKey ? WizardI18n(placeholderKey) : ""; + return placeholderKey ? I18n.t(placeholderKey) : ""; }, }); diff --git a/assets/javascripts/wizard/components/wizard-time-input.js.es6 b/assets/javascripts/discourse/components/custom-wizard-time-input.js.es6 similarity index 61% rename from assets/javascripts/wizard/components/wizard-time-input.js.es6 rename to assets/javascripts/discourse/components/custom-wizard-time-input.js.es6 index 0bca244d..c09ed110 100644 --- a/assets/javascripts/wizard/components/wizard-time-input.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-time-input.js.es6 @@ -1,3 +1,3 @@ import TimeInput from "discourse/components/time-input"; -export default TimeInput.extend(); +export default TimeInput.extend({}); diff --git a/assets/javascripts/wizard/components/field-validators.js.es6 b/assets/javascripts/discourse/components/field-validators.js.es6 similarity index 99% rename from assets/javascripts/wizard/components/field-validators.js.es6 rename to assets/javascripts/discourse/components/field-validators.js.es6 index a315020d..8b9b39da 100644 --- a/assets/javascripts/wizard/components/field-validators.js.es6 +++ b/assets/javascripts/discourse/components/field-validators.js.es6 @@ -1,4 +1,5 @@ import Component from "@ember/component"; + export default Component.extend({ actions: { perform() { diff --git a/assets/javascripts/wizard/components/similar-topics-validator.js.es6 b/assets/javascripts/discourse/components/similar-topics-validator.js.es6 similarity index 97% rename from assets/javascripts/wizard/components/similar-topics-validator.js.es6 rename to assets/javascripts/discourse/components/similar-topics-validator.js.es6 index 98ea9270..634af983 100644 --- a/assets/javascripts/wizard/components/similar-topics-validator.js.es6 +++ b/assets/javascripts/discourse/components/similar-topics-validator.js.es6 @@ -1,4 +1,4 @@ -import WizardFieldValidator from "../../wizard/components/validator"; +import WizardFieldValidator from "discourse/plugins/discourse-custom-wizard/discourse/components/validator"; import { deepMerge } from "discourse-common/lib/object"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { cancel, later } from "@ember/runloop"; diff --git a/assets/javascripts/wizard/components/validator.js.es6 b/assets/javascripts/discourse/components/validator.js.es6 similarity index 84% rename from assets/javascripts/wizard/components/validator.js.es6 rename to assets/javascripts/discourse/components/validator.js.es6 index 6227cc64..3c19cc3d 100644 --- a/assets/javascripts/wizard/components/validator.js.es6 +++ b/assets/javascripts/discourse/components/validator.js.es6 @@ -1,7 +1,6 @@ import Component from "@ember/component"; import { equal } from "@ember/object/computed"; -import { ajax } from "discourse/lib/ajax"; -import { getToken } from "wizard/lib/ajax"; +import { ajax, getToken } from "discourse/lib/ajax"; export default Component.extend({ classNames: ["validator"], @@ -10,7 +9,6 @@ export default Component.extend({ invalidMessageKey: null, isValid: null, isInvalid: equal("isValid", false), - layoutName: "components/validator", // useful for sharing the template with extending components init() { this._super(...arguments); diff --git a/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 b/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 deleted file mode 100644 index c6e1fd9c..00000000 --- a/assets/javascripts/discourse/components/wizard-advanced-toggle.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import { default as discourseComputed } from "discourse-common/utils/decorators"; -import Component from "@ember/component"; - -export default Component.extend({ - classNames: "wizard-advanced-toggle", - - @discourseComputed("showAdvanced") - toggleClass(showAdvanced) { - let classes = "btn"; - if (showAdvanced) { - classes += " btn-primary"; - } - return classes; - }, - - actions: { - toggleAdvanced() { - this.toggleProperty("showAdvanced"); - }, - }, -}); diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index feb83754..81a12530 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -1,8 +1,7 @@ import { default as discourseComputed } from "discourse-common/utils/decorators"; -import { and, empty, equal, or } from "@ember/object/computed"; +import { empty, equal, or } from "@ember/object/computed"; import { notificationLevels, selectKitContent } from "../lib/wizard"; import { computed } from "@ember/object"; -import wizardSchema from "../lib/wizard-schema"; import UndoChanges from "../mixins/undo-changes"; import Component from "@ember/component"; import I18n from "I18n"; @@ -16,6 +15,7 @@ export default Component.extend(UndoChanges, { createTopic: equal("action.type", "create_topic"), updateProfile: equal("action.type", "update_profile"), watchCategories: equal("action.type", "watch_categories"), + watchTags: equal("action.type", "watch_tags"), sendMessage: equal("action.type", "send_message"), openComposer: equal("action.type", "open_composer"), sendToApi: equal("action.type", "send_to_api"), @@ -25,8 +25,6 @@ export default Component.extend(UndoChanges, { createGroup: equal("action.type", "create_group"), apiEmpty: empty("action.api"), groupPropertyTypes: selectKitContent(["id", "name"]), - hasAdvanced: or("hasCustomFields", "routeTo"), - showAdvanced: and("hasAdvanced", "action.type"), hasCustomFields: or( "basicTopicFields", "updateProfile", @@ -36,22 +34,14 @@ export default Component.extend(UndoChanges, { basicTopicFields: or("createTopic", "sendMessage", "openComposer"), publicTopicFields: or("createTopic", "openComposer"), showPostAdvanced: or("createTopic", "sendMessage"), - actionTypes: Object.keys(wizardSchema.action.types).map((type) => { - return { - id: type, - name: I18n.t(`admin.wizard.action.${type}.label`), - }; - }), availableNotificationLevels: notificationLevels.map((type) => { return { id: type, - name: I18n.t( - `admin.wizard.action.watch_categories.notification_level.${type}` - ), + name: I18n.t(`admin.wizard.action.watch_x.notification_level.${type}`), }; }), - messageUrl: "https://thepavilion.io/t/2810", + messageUrl: "https://discourse.pluginmanager.org/t/action-settings", @discourseComputed("action.type") messageKey(type) { @@ -101,4 +91,14 @@ export default Component.extend(UndoChanges, { } return apis.find((a) => a.name === api).endpoints; }, + + @discourseComputed("fieldTypes") + hasEventField(fieldTypes) { + return fieldTypes.map((ft) => ft.id).includes("event"); + }, + + @discourseComputed("fieldTypes") + hasLocationField(fieldTypes) { + return fieldTypes.map((ft) => ft.id).includes("location"); + }, }); diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index 8efb7f0c..ed66012a 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -1,5 +1,5 @@ import { default as discourseComputed } from "discourse-common/utils/decorators"; -import { alias, equal, or } from "@ember/object/computed"; +import { equal, or } from "@ember/object/computed"; import { computed } from "@ember/object"; import { selectKitContent } from "../lib/wizard"; import UndoChanges from "../mixins/undo-changes"; @@ -27,8 +27,7 @@ export default Component.extend(UndoChanges, { isTextType: or("isText", "isTextarea", "isComposer"), isComposerPreview: equal("field.type", "composer_preview"), categoryPropertyTypes: selectKitContent(["id", "slug"]), - showAdvanced: alias("field.type"), - messageUrl: "https://thepavilion.io/t/2809", + messageUrl: "https://discourse.pluginmanager.org/t/field-settings", @discourseComputed("field.type") validations(type) { @@ -144,11 +143,17 @@ export default Component.extend(UndoChanges, { actions: { imageUploadDone(upload) { - this.set("field.image", upload.url); + this.setProperties({ + "field.image": upload.url, + "field.image_upload_id": upload.id, + }); }, imageUploadDeleted() { - this.set("field.image", null); + this.setProperties({ + "field.image": null, + "field.image_upload_id": null, + }); }, }, }); diff --git a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 index 2a07dd65..7605d8a4 100644 --- a/assets/javascripts/discourse/components/wizard-custom-step.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-step.js.es6 @@ -24,11 +24,17 @@ export default Component.extend({ actions: { bannerUploadDone(upload) { - this.set("step.banner", upload.url); + this.setProperties({ + "step.banner": upload.url, + "step.banner_upload_id": upload.id, + }); }, bannerUploadDeleted() { - this.set("step.banner", null); + this.setProperties({ + "step.banner": null, + "step.banner_upload_id": null, + }); }, }, }); diff --git a/assets/javascripts/discourse/components/wizard-links.js.es6 b/assets/javascripts/discourse/components/wizard-links.js.es6 index 62c45911..6d02987a 100644 --- a/assets/javascripts/discourse/components/wizard-links.js.es6 +++ b/assets/javascripts/discourse/components/wizard-links.js.es6 @@ -71,6 +71,17 @@ export default Component.extend({ }); }, + getNextIndex() { + const items = this.items; + if (!items || items.length === 0) { + return 0; + } + const numbers = items + .map((i) => Number(i.id.split("_").pop())) + .sort((a, b) => a - b); + return numbers[numbers.length - 1]; + }, + actions: { add() { const items = this.get("items"); @@ -78,16 +89,9 @@ export default Component.extend({ let params = setWizardDefaults({}, itemType); params.isNew = true; + params.index = this.getNextIndex(); - let index = 0; - if (items.length) { - let last_item = items[items.length - 1]; - index = Number(last_item.id.split("_").pop()); - } - - params.index = index; - - let id = `${itemType}_${index + 1}`; + let id = `${itemType}_${params.index + 1}`; if (itemType === "field") { id = `${this.parentId}_${id}`; } diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index 8a2ae9ce..a257ed12 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -377,7 +377,7 @@ export default Component.extend({ this.changeValue(event.target.value); }, - changeUserValue(previousValue, value) { + changeUserValue(value) { this.changeValue(value); }, }, diff --git a/assets/javascripts/discourse/components/wizard-message.js.es6 b/assets/javascripts/discourse/components/wizard-message.js.es6 index b273e78b..686a7254 100644 --- a/assets/javascripts/discourse/components/wizard-message.js.es6 +++ b/assets/javascripts/discourse/components/wizard-message.js.es6 @@ -6,6 +6,7 @@ import I18n from "I18n"; const icons = { error: "times-circle", success: "check-circle", + warn: "exclamation-circle", info: "info-circle", }; diff --git a/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 b/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 index 8332b86e..b1d8a0f5 100644 --- a/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 +++ b/assets/javascripts/discourse/components/wizard-realtime-validations.js.es6 @@ -6,7 +6,8 @@ import discourseComputed from "discourse-common/utils/decorators"; import I18n from "I18n"; export default Component.extend({ - classNames: ["realtime-validations"], + classNames: ["realtime-validations", "setting", "full", "subscription"], + @discourseComputed timeUnits() { return ["days", "weeks", "months", "years"].map((unit) => { diff --git a/assets/javascripts/discourse/components/wizard-subscription-badge.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-badge.js.es6 new file mode 100644 index 00000000..301c618e --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-badge.js.es6 @@ -0,0 +1,30 @@ +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; +import Subscription from "../mixins/subscription"; +import DiscourseURL from "discourse/lib/url"; +import I18n from "I18n"; + +export default Component.extend(Subscription, { + tagName: "a", + classNameBindings: [":wizard-subscription-badge", "subscriptionType"], + attributeBindings: ["title"], + + @discourseComputed("subscriptionType") + i18nKey(type) { + return `admin.wizard.subscription.type.${type ? type : "none"}`; + }, + + @discourseComputed("i18nKey") + title(i18nKey) { + return I18n.t(`${i18nKey}.title`); + }, + + @discourseComputed("i18nKey") + label(i18nKey) { + return I18n.t(`${i18nKey}.label`); + }, + + click() { + DiscourseURL.routeTo(this.subscriptionLink); + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-subscription-container.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-container.js.es6 new file mode 100644 index 00000000..5cc6b17c --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-container.js.es6 @@ -0,0 +1,26 @@ +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; +import Subscription from "../mixins/subscription"; + +export default Component.extend(Subscription, { + classNameBindings: [":wizard-subscription-container", "subscribed"], + + @discourseComputed("subscribed") + subscribedIcon(subscribed) { + return subscribed ? "check" : "dash"; + }, + + @discourseComputed("subscribed") + subscribedLabel(subscribed) { + return `admin.wizard.subscription.${ + subscribed ? "subscribed" : "not_subscribed" + }.label`; + }, + + @discourseComputed("subscribed") + subscribedTitle(subscribed) { + return `admin.wizard.subscription.${ + subscribed ? "subscribed" : "not_subscribed" + }.title`; + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-subscription-cta.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-cta.js.es6 new file mode 100644 index 00000000..f483fbe8 --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-cta.js.es6 @@ -0,0 +1,36 @@ +import Component from "@ember/component"; +import discourseComputed from "discourse-common/utils/decorators"; +import Subscription from "../mixins/subscription"; +import I18n from "I18n"; + +export default Component.extend(Subscription, { + tagName: "a", + classNameBindings: [":btn", ":btn-pavilion-support", "subscriptionType"], + attributeBindings: ["title"], + + @discourseComputed("subscribed") + i18nKey(subscribed) { + return `admin.wizard.subscription.cta.${ + subscribed ? "subscribed" : "none" + }`; + }, + + @discourseComputed("subscribed") + icon(subscribed) { + return subscribed ? "far-life-ring" : "external-link-alt"; + }, + + @discourseComputed("i18nKey") + title(i18nKey) { + return I18n.t(`${i18nKey}.title`); + }, + + @discourseComputed("i18nKey") + label(i18nKey) { + return I18n.t(`${i18nKey}.label`); + }, + + click() { + window.open(this.subscriptionCtaLink, "_blank").focus(); + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 new file mode 100644 index 00000000..53f7d19c --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 @@ -0,0 +1,96 @@ +import SingleSelectComponent from "select-kit/components/single-select"; +import Subscription from "../mixins/subscription"; +import wizardSchema from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-schema"; +import discourseComputed from "discourse-common/utils/decorators"; +import I18n from "I18n"; + +const nameKey = function (feature, attribute, value) { + if (feature === "action") { + return `admin.wizard.action.${value}.label`; + } else { + return `admin.wizard.${feature}.${attribute}.${value}`; + } +}; + +export default SingleSelectComponent.extend(Subscription, { + classNames: ["combo-box", "wizard-subscription-selector"], + + selectKitOptions: { + autoFilterable: false, + filterable: false, + showFullTitle: true, + headerComponent: + "wizard-subscription-selector/wizard-subscription-selector-header", + caretUpIcon: "caret-up", + caretDownIcon: "caret-down", + }, + + allowedSubscriptionTypes(feature, attribute, value) { + let attributes = this.subscriptionAttributes[feature]; + if (!attributes || !attributes[attribute]) { + return ["none"]; + } + let allowedTypes = []; + Object.keys(attributes[attribute]).forEach((subscriptionType) => { + let values = attributes[attribute][subscriptionType]; + if (values[0] === "*" || values.includes(value)) { + allowedTypes.push(subscriptionType); + } + }); + return allowedTypes; + }, + + @discourseComputed("feature", "attribute") + content(feature, attribute) { + return wizardSchema[feature][attribute] + .map((value) => { + let allowedSubscriptionTypes = this.allowedSubscriptionTypes( + feature, + attribute, + value + ); + + let subscriptionRequired = + allowedSubscriptionTypes.length && + !allowedSubscriptionTypes.includes("none"); + + let attrs = { + id: value, + name: I18n.t(nameKey(feature, attribute, value)), + subscriptionRequired, + }; + + if (subscriptionRequired) { + let subscribed = allowedSubscriptionTypes.includes( + this.subscriptionType + ); + let selectorKey = subscribed ? "subscribed" : "not_subscribed"; + let selectorLabel = `admin.wizard.subscription.${selectorKey}.selector`; + + attrs.disabled = !subscribed; + attrs.selectorLabel = selectorLabel; + } + + return attrs; + }) + .sort(function (a, b) { + if (a.subscriptionType && !b.subscriptionType) { + return 1; + } + if (!a.subscriptionType && b.subscriptionType) { + return -1; + } + if (a.subscriptionType === b.subscriptionType) { + return a.subscriptionType + ? a.subscriptionType.localeCompare(b.subscriptionType) + : 0; + } else { + return a.subscriptionType === "standard" ? -1 : 0; + } + }); + }, + + modifyComponentForRow() { + return "wizard-subscription-selector/wizard-subscription-selector-row"; + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-header.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-header.js.es6 new file mode 100644 index 00000000..74f29f08 --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-header.js.es6 @@ -0,0 +1,17 @@ +import SingleSelectHeaderComponent from "select-kit/components/select-kit/single-select-header"; +import { computed } from "@ember/object"; +import { reads } from "@ember/object/computed"; + +export default SingleSelectHeaderComponent.extend({ + classNames: ["combo-box-header", "wizard-subscription-selector-header"], + caretUpIcon: reads("selectKit.options.caretUpIcon"), + caretDownIcon: reads("selectKit.options.caretDownIcon"), + caretIcon: computed( + "selectKit.isExpanded", + "caretUpIcon", + "caretDownIcon", + function () { + return this.selectKit.isExpanded ? this.caretUpIcon : this.caretDownIcon; + } + ), +}); diff --git a/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-row.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-row.js.es6 new file mode 100644 index 00000000..1d43047a --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-subscription-selector/wizard-subscription-selector-row.js.es6 @@ -0,0 +1,20 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; +import { default as discourseComputed } from "discourse-common/utils/decorators"; + +export default SelectKitRowComponent.extend({ + classNameBindings: ["isDisabled:disabled"], + + @discourseComputed("item") + isDisabled() { + return this.item.disabled; + }, + + click(event) { + event.preventDefault(); + event.stopPropagation(); + if (!this.item.disabled) { + this.selectKit.select(this.rowValue, this.item); + } + return false; + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-table-field.js.es6 b/assets/javascripts/discourse/components/wizard-table-field.js.es6 new file mode 100644 index 00000000..93859f1f --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-table-field.js.es6 @@ -0,0 +1,139 @@ +import Component from "@ember/component"; +import { action } from "@ember/object"; +import { equal, notEmpty } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; +import I18n from "I18n"; + +export default Component.extend({ + classNameBindings: ["value.type"], + isText: equal("value.type", "text"), + isComposer: equal("value.type", "composer"), + isDate: equal("value.type", "date"), + isTime: equal("value.type", "time"), + isDateTime: equal("value.type", "date_time"), + isNumber: equal("value.type", "number"), + isCheckbox: equal("value.type", "checkbox"), + isUrl: equal("value.type", "url"), + isUpload: equal("value.type", "upload"), + isDropdown: equal("value.type", "dropdown"), + isTag: equal("value.type", "tag"), + isCategory: equal("value.type", "category"), + isGroup: equal("value.type", "group"), + isUserSelector: equal("value.type", "user_selector"), + isSubmittedAt: equal("field", "submitted_at"), + isComposerPreview: equal("value.type", "composer_preview"), + textState: "text-collapsed", + toggleText: I18n.t("admin.wizard.expand_text"), + + @discourseComputed("value", "isUser", "isSubmittedAt") + hasValue(value, isUser, isSubmittedAt) { + if (isUser || isSubmittedAt) { + return value; + } + return value && value.value; + }, + + @discourseComputed("field", "value.type") + isUser(field, type) { + return field === "username" || field === "user" || type === "user"; + }, + + @discourseComputed("value.type") + isLongtext(type) { + return type === "textarea" || type === "long_text"; + }, + + @discourseComputed("value") + checkboxValue(value) { + const isCheckbox = this.get("isCheckbox"); + if (isCheckbox) { + if (value.value.includes("true")) { + return true; + } else if (value.value.includes("false")) { + return false; + } + } + }, + + @action + expandText() { + const state = this.get("textState"); + + if (state === "text-collapsed") { + this.set("textState", "text-expanded"); + this.set("toggleText", I18n.t("admin.wizard.collapse_text")); + } else if (state === "text-expanded") { + this.set("textState", "text-collapsed"); + this.set("toggleText", I18n.t("admin.wizard.expand_text")); + } + }, + + @discourseComputed("value") + file(value) { + const isUpload = this.get("isUpload"); + if (isUpload) { + return value.value; + } + }, + + @discourseComputed("value") + submittedUsers(value) { + const isUserSelector = this.get("isUserSelector"); + const users = []; + + if (isUserSelector) { + const userData = value.value; + const usernames = []; + + if (userData.indexOf(",")) { + usernames.push(...userData.split(",")); + + usernames.forEach((u) => { + const user = { + username: u, + url: `/u/${u}`, + }; + users.push(user); + }); + } + } + return users; + }, + + @discourseComputed("isUser", "field", "value") + username(isUser, field, value) { + if (isUser) { + return value.username; + } + if (field === "username") { + return value.value; + } + return null; + }, + + showUsername: notEmpty("username"), + + @discourseComputed("username") + userProfileUrl(username) { + if (username) { + return `/u/${username}`; + } + return "/"; + }, + + @discourseComputed("value") + categoryUrl(value) { + const isCategory = this.get("isCategory"); + if (isCategory) { + return `/c/${value.value}`; + } + }, + + @discourseComputed("value") + groupUrl(value) { + const isGroup = this.get("isGroup"); + if (isGroup) { + return `/g/${value.value}`; + } + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 index 88d7200c..b6d07cef 100644 --- a/assets/javascripts/discourse/components/wizard-text-editor.js.es6 +++ b/assets/javascripts/discourse/components/wizard-text-editor.js.es6 @@ -5,11 +5,7 @@ import { scheduleOnce } from "@ember/runloop"; import Component from "@ember/component"; import I18n from "I18n"; -const excludedUserProperties = [ - "avatar", - "profile_background", - "card_background", -]; +const excludedUserProperties = ["profile_background", "card_background"]; export default Component.extend({ classNames: "wizard-text-editor", @@ -52,12 +48,12 @@ export default Component.extend({ @discourseComputed("wizardFields") wizardFieldList(wizardFields) { - return wizardFields.map((f) => ` w{${f.id}}`); + return (wizardFields || []).map((f) => ` w{${f.id}}`); }, @discourseComputed("wizardActions") wizardActionList(wizardActions) { - return wizardActions.map((a) => ` w{${a.id}}`); + return (wizardActions || []).map((a) => ` w{${a.id}}`); }, actions: { diff --git a/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs b/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs index f76722fc..f893d4ac 100644 --- a/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs +++ b/assets/javascripts/discourse/connectors/admin-menu/wizards-nav-button.hbs @@ -1,3 +1,7 @@ {{#if currentUser.admin}} {{nav-item route="adminWizards" label="admin.wizard.nav_label"}} + + {{#if wizardErrorNotice}} + {{d-icon "exclaimation-circle"}} + {{/if}} {{/if}} diff --git a/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.hbs b/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.hbs index 4b5d673d..5ce96f2f 100644 --- a/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.hbs +++ b/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.hbs @@ -6,11 +6,11 @@
{{combo-box - value=wizardListVal - content=wizardList - onChange=(action "changeWizard") - options=(hash - none="admin.wizard.select" - )}} + value=wizardListVal + content=wizardList + onChange=(action "changeWizard") + options=(hash + none="admin.wizard.select" + )}}
diff --git a/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.js.es6 b/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.js.es6 index 16352f95..7004c317 100644 --- a/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.js.es6 +++ b/assets/javascripts/discourse/connectors/category-custom-settings/custom-wizard-category-settings.js.es6 @@ -1,9 +1,9 @@ -import CustomWizard from "../../models/custom-wizard"; +import CustomWizardAdmin from "../../models/custom-wizard-admin"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default { setupComponent(attrs, component) { - CustomWizard.all() + CustomWizardAdmin.all() .then((result) => { component.set("wizardList", result); }) diff --git a/assets/javascripts/discourse/controllers/admin-wizards-api-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-api-show.js.es6 index 5dba2d7f..b89cc447 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-api-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-api-show.js.es6 @@ -11,7 +11,7 @@ export default Controller.extend({ queryParams: ["refresh_list"], loadingSubscriptions: false, notAuthorized: not("api.authorized"), - endpointMethods: selectKitContent(["GET", "PUT", "POST", "PATCH", "DELETE"]), + endpointMethods: selectKitContent(["PUT", "POST", "PATCH", "DELETE"]), showRemove: not("isNew"), showRedirectUri: and("threeLeggedOauth", "api.name"), responseIcon: null, @@ -88,6 +88,11 @@ export default Controller.extend({ twoLeggedOauth: equal("api.authType", "oauth_2"), threeLeggedOauth: equal("api.authType", "oauth_3"), + @discourseComputed("api.isNew") + nameClass(isNew) { + return isNew ? "new" : "saved"; + }, + actions: { addParam() { this.get("api.authParams").pushObject({}); @@ -149,7 +154,6 @@ export default Controller.extend({ const api = this.get("api"); const name = api.name; const authType = api.authType; - let refreshList = false; // eslint-disable-line let error; if (!name || !authType) { @@ -164,11 +168,6 @@ export default Controller.extend({ data["title"] = api.title; } - const originalTitle = this.get("api.originalTitle"); - if (api.get("isNew") || (originalTitle && api.title !== originalTitle)) { - refreshList = true; - } - if (api.get("isNew")) { data["new"] = true; } diff --git a/assets/javascripts/discourse/controllers/admin-wizards-columns.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-columns.js.es6 new file mode 100644 index 00000000..4754c577 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-columns.js.es6 @@ -0,0 +1,14 @@ +import Controller from "@ember/controller"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; + +export default Controller.extend(ModalFunctionality, { + actions: { + save() { + this.send("closeModal"); + }, + + resetToDefault() { + this.get("model.reset")(); + }, + }, +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 index 404c6afd..5d47d8d7 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 @@ -4,7 +4,7 @@ import CustomWizardCustomField from "../models/custom-wizard-custom-field"; export default Controller.extend({ messageKey: "create", fieldKeys: ["klass", "type", "name", "serializers"], - documentationUrl: "https://thepavilion.io/t/3572", + documentationUrl: "https://discourse.pluginmanager.org/t/custom-fields", actions: { addField() { diff --git a/assets/javascripts/discourse/controllers/admin-wizards-logs-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-logs-show.js.es6 new file mode 100644 index 00000000..7e3fdff1 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-logs-show.js.es6 @@ -0,0 +1,52 @@ +import discourseComputed from "discourse-common/utils/decorators"; +import { notEmpty } from "@ember/object/computed"; +import CustomWizardLogs from "../models/custom-wizard-logs"; +import Controller from "@ember/controller"; + +export default Controller.extend({ + refreshing: false, + hasLogs: notEmpty("logs"), + page: 0, + canLoadMore: true, + logs: [], + messageKey: "viewing", + + loadLogs() { + if (!this.canLoadMore) { + return; + } + const page = this.get("page"); + const wizardId = this.get("wizard.id"); + + this.set("refreshing", true); + + CustomWizardLogs.list(wizardId, page) + .then((result) => { + this.set("logs", this.logs.concat(result.logs)); + }) + .finally(() => this.set("refreshing", false)); + }, + + @discourseComputed("hasLogs", "refreshing") + noResults(hasLogs, refreshing) { + return !hasLogs && !refreshing; + }, + + actions: { + loadMore() { + if (!this.loadingMore && this.logs.length < this.total) { + this.set("page", (this.page += 1)); + this.loadLogs(); + } + }, + + refresh() { + this.setProperties({ + canLoadMore: true, + page: 0, + logs: [], + }); + this.loadLogs(); + }, + }, +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 index 9559b01b..7388a8d6 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-logs.js.es6 @@ -1,50 +1,34 @@ -import discourseComputed from "discourse-common/utils/decorators"; -import { notEmpty } from "@ember/object/computed"; -import CustomWizardLogs from "../models/custom-wizard-logs"; import Controller from "@ember/controller"; +import { default as discourseComputed } from "discourse-common/utils/decorators"; export default Controller.extend({ - refreshing: false, - hasLogs: notEmpty("logs"), - page: 0, - canLoadMore: true, - logs: [], + documentationUrl: "https://thepavilion.io/t/2818", - loadLogs() { - if (!this.canLoadMore) { - return; + @discourseComputed("wizardId") + wizardName(wizardId) { + let currentWizard = this.wizardList.find( + (wizard) => wizard.id === wizardId + ); + if (currentWizard) { + return currentWizard.name; + } + }, + + @discourseComputed("wizardName") + messageOpts(wizardName) { + return { + wizardName, + }; + }, + + @discourseComputed("wizardId") + messageKey(wizardId) { + let key = "select"; + + if (wizardId) { + key = "viewing"; } - this.set("refreshing", true); - - CustomWizardLogs.list() - .then((result) => { - if (!result || result.length === 0) { - this.set("canLoadMore", false); - } - this.set("logs", this.logs.concat(result)); - }) - .finally(() => this.set("refreshing", false)); - }, - - @discourseComputed("hasLogs", "refreshing") - noResults(hasLogs, refreshing) { - return !hasLogs && !refreshing; - }, - - actions: { - loadMore() { - this.set("page", (this.page += 1)); - this.loadLogs(); - }, - - refresh() { - this.setProperties({ - canLoadMore: true, - page: 0, - logs: [], - }); - this.loadLogs(); - }, + return key; }, }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 index 7228d164..67016bd7 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-manager.js.es6 @@ -7,7 +7,7 @@ import I18n from "I18n"; import { underscore } from "@ember/string"; export default Controller.extend({ - messageUrl: "https://thepavilion.io/t/3652", + messageUrl: "https://discourse.pluginmanager.org/t/wizard-manager", messageKey: "info", messageIcon: "info-circle", messageClass: "info", @@ -68,7 +68,7 @@ export default Controller.extend({ file: null, filename: null, }); - $("#file-upload").val(""); + document.getElementById("custom-wizard-file-upload").value = ""; }, @observes("importing", "destroying") @@ -83,7 +83,7 @@ export default Controller.extend({ actions: { upload() { - $("#file-upload").click(); + document.getElementById("custom-wizard-file-upload").click(); }, clearFile() { @@ -102,7 +102,7 @@ export default Controller.extend({ if (maxFileSize < file.size) { this.setMessage("error", "file_size_error"); this.set("file", null); - $("#file-upload").val(""); + document.getElementById("custom-wizard-file-upload").value = ""; } else { this.setProperties({ file, diff --git a/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 index f5f9926d..41dabbb4 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-submissions-show.js.es6 @@ -1,6 +1,72 @@ import Controller from "@ember/controller"; +import { empty } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; +import showModal from "discourse/lib/show-modal"; +import CustomWizardAdmin from "../models/custom-wizard-admin"; +import { formatModel } from "../lib/wizard-submission"; export default Controller.extend({ downloadUrl: fmt("wizard.id", "/admin/wizards/submissions/%@/download"), + noResults: empty("submissions"), + page: 0, + total: 0, + + loadMoreSubmissions() { + const page = this.get("page"); + const wizardId = this.get("wizard.id"); + + this.set("loadingMore", true); + CustomWizardAdmin.submissions(wizardId, page) + .then((result) => { + if (result.submissions) { + const { submissions } = formatModel(result); + + this.get("submissions").pushObjects(submissions); + } + }) + .finally(() => { + this.set("loadingMore", false); + }); + }, + + @discourseComputed("submissions.[]", "fields.@each.enabled") + displaySubmissions(submissions, fields) { + let result = []; + + submissions.forEach((submission) => { + let sub = {}; + + Object.keys(submission).forEach((fieldId) => { + if (fields.some((f) => f.id === fieldId && f.enabled)) { + sub[fieldId] = submission[fieldId]; + } + }); + result.push(sub); + }); + + return result; + }, + + actions: { + loadMore() { + if (!this.loadingMore && this.submissions.length < this.total) { + this.set("page", this.get("page") + 1); + this.loadMoreSubmissions(); + } + }, + + showEditColumnsModal() { + return showModal("admin-wizards-columns", { + model: { + columns: this.get("fields"), + reset: () => { + this.get("fields").forEach((field) => { + field.set("enabled", true); + }); + }, + }, + }); + }, + }, }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 new file mode 100644 index 00000000..7388a8d6 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards-submissions.js.es6 @@ -0,0 +1,34 @@ +import Controller from "@ember/controller"; +import { default as discourseComputed } from "discourse-common/utils/decorators"; + +export default Controller.extend({ + documentationUrl: "https://thepavilion.io/t/2818", + + @discourseComputed("wizardId") + wizardName(wizardId) { + let currentWizard = this.wizardList.find( + (wizard) => wizard.id === wizardId + ); + if (currentWizard) { + return currentWizard.name; + } + }, + + @discourseComputed("wizardName") + messageOpts(wizardName) { + return { + wizardName, + }; + }, + + @discourseComputed("wizardId") + messageKey(wizardId) { + let key = "select"; + + if (wizardId) { + key = "viewing"; + } + + return key; + }, +}); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 index 1eeb62e6..c9a80e0e 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 @@ -36,7 +36,8 @@ export default Controller.extend({ @discourseComputed("wizard.id") wizardUrl(wizardId) { - return window.location.origin + "/w/" + dasherize(wizardId); + let baseUrl = window.location.href.split("/admin"); + return baseUrl[0] + "/w/" + dasherize(wizardId); }, @discourseComputed("wizard.after_time_scheduled") @@ -92,7 +93,11 @@ export default Controller.extend({ wizard .save(opts) .then((result) => { - this.send("afterSave", result.wizard_id); + if (result.wizard_id) { + this.send("afterSave", result.wizard_id); + } else if (result.errors) { + this.set("error", result.errors.join(", ")); + } }) .catch((result) => { this.set("error", this.getErrorMessage(result)); @@ -118,10 +123,6 @@ export default Controller.extend({ controller.setup(); }, - toggleAdvanced() { - this.toggleProperty("wizard.showAdvanced"); - }, - copyUrl() { const $copyRange = $('

'); $copyRange.html(this.wizardUrl); diff --git a/assets/javascripts/discourse/controllers/admin-wizards-wizard.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-wizard.js.es6 index ddd63337..5178a1e1 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-wizard.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-wizard.js.es6 @@ -21,5 +21,6 @@ export default Controller.extend({ return key; }, - messageUrl: "https://thepavilion.io/c/knowledge/discourse/custom-wizard", + messageUrl: + "https://discourse.pluginmanager.org/c/discourse-custom-wizard/documentation", }); diff --git a/assets/javascripts/discourse/controllers/admin-wizards.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 new file mode 100644 index 00000000..518893b6 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-wizards.js.es6 @@ -0,0 +1,9 @@ +import Controller from "@ember/controller"; +import { equal, or } from "@ember/object/computed"; + +export default Controller.extend({ + businessSubscription: equal("subscriptionType", "business"), + communitySubscription: equal("subscriptionType", "community"), + standardSubscription: equal("subscriptionType", "standard"), + showApi: or("businessSubscription", "communitySubscription"), +}); diff --git a/assets/javascripts/discourse/controllers/custom-wizard-index.js.es6 b/assets/javascripts/discourse/controllers/custom-wizard-index.js.es6 new file mode 100644 index 00000000..f56db02d --- /dev/null +++ b/assets/javascripts/discourse/controllers/custom-wizard-index.js.es6 @@ -0,0 +1,24 @@ +import Controller from "@ember/controller"; +import { or } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; + +const reasons = { + noWizard: "none", + requiresLogin: "requires_login", + notPermitted: "not_permitted", + completed: "completed", +}; + +export default Controller.extend({ + noAccess: or("noWizard", "requiresLogin", "notPermitted", "completed"), + + @discourseComputed("noAccessReason") + noAccessI18nKey(reason) { + return reason ? `wizard.${reasons[reason]}` : "wizard.none"; + }, + + @discourseComputed + noAccessReason() { + return Object.keys(reasons).find((reason) => this.get(reason)); + }, +}); diff --git a/assets/javascripts/wizard/controllers/custom-step.js.es6 b/assets/javascripts/discourse/controllers/custom-wizard-step.js.es6 similarity index 74% rename from assets/javascripts/wizard/controllers/custom-step.js.es6 rename to assets/javascripts/discourse/controllers/custom-wizard-step.js.es6 index b44c0fca..2dca2e70 100644 --- a/assets/javascripts/wizard/controllers/custom-step.js.es6 +++ b/assets/javascripts/discourse/controllers/custom-wizard-step.js.es6 @@ -1,7 +1,10 @@ -import StepController from "wizard/controllers/step"; +import Controller from "@ember/controller"; import getUrl from "discourse-common/lib/get-url"; -export default StepController.extend({ +export default Controller.extend({ + wizard: null, + step: null, + actions: { goNext(response) { let nextStepId = response["next_step_id"]; @@ -12,12 +15,12 @@ export default StepController.extend({ const wizardId = this.get("wizard.id"); window.location.href = getUrl(`/w/${wizardId}/steps/${nextStepId}`); } else { - this.transitionToRoute("custom.step", nextStepId); + this.transitionToRoute("customWizardStep", nextStepId); } }, goBack() { - this.transitionToRoute("custom.step", this.get("step.previous")); + this.transitionToRoute("customWizardStep", this.get("step.previous")); }, showMessage(message) { diff --git a/assets/javascripts/discourse/controllers/custom-wizard.js.es6 b/assets/javascripts/discourse/controllers/custom-wizard.js.es6 new file mode 100644 index 00000000..35f964e2 --- /dev/null +++ b/assets/javascripts/discourse/controllers/custom-wizard.js.es6 @@ -0,0 +1,5 @@ +import Controller from "@ember/controller"; + +export default Controller.extend({ + queryParams: ["reset"], +}); diff --git a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 index 90ab5359..272e276e 100644 --- a/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 +++ b/assets/javascripts/discourse/custom-wizard-admin-route-map.js.es6 @@ -43,7 +43,16 @@ export default { } ); - this.route("adminWizardsLogs", { path: "/logs", resetNamespace: true }); + this.route( + "adminWizardsLogs", + { path: "/logs", resetNamespace: true }, + function () { + this.route("adminWizardsLogsShow", { + path: "/:wizardId/", + resetNamespace: true, + }); + } + ); this.route("adminWizardsManager", { path: "/manager", diff --git a/assets/javascripts/discourse/custom-wizard-route-map.js.es6 b/assets/javascripts/discourse/custom-wizard-route-map.js.es6 new file mode 100644 index 00000000..08606301 --- /dev/null +++ b/assets/javascripts/discourse/custom-wizard-route-map.js.es6 @@ -0,0 +1,12 @@ +export default function () { + this.route( + "customWizard", + { path: "/w/:wizard_id", resetNamespace: true }, + function () { + this.route("customWizardStep", { + path: "/steps/:step_id", + resetNamespace: true, + }); + } + ); +} diff --git a/assets/javascripts/wizard/helpers/char-counter.js.es6 b/assets/javascripts/discourse/helpers/char-counter.js.es6 similarity index 100% rename from assets/javascripts/wizard/helpers/char-counter.js.es6 rename to assets/javascripts/discourse/helpers/char-counter.js.es6 diff --git a/assets/javascripts/discourse/helpers/custom-wizard.js.es6 b/assets/javascripts/discourse/helpers/custom-wizard.js.es6 deleted file mode 100644 index fb5063cc..00000000 --- a/assets/javascripts/discourse/helpers/custom-wizard.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -import { registerUnbound } from "discourse-common/lib/helpers"; -import { dasherize } from "@ember/string"; - -registerUnbound("dasherize", function (string) { - return dasherize(string); -}); diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 20787b4e..2d13e703 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -1,11 +1,12 @@ import DiscourseURL from "discourse/lib/url"; import { withPluginApi } from "discourse/lib/plugin-api"; import getUrl from "discourse-common/lib/get-url"; +import { observes } from "discourse-common/utils/decorators"; export default { name: "custom-wizard-edits", initialize(container) { - const siteSettings = container.lookup("site-settings:main"); + const siteSettings = container.lookup("service:site-settings"); if (!siteSettings.custom_wizard_enabled) { return; @@ -19,7 +20,7 @@ export default { return existing.apply(this, [path, opts]); }; - withPluginApi("0.8.7", (api) => { + withPluginApi("0.8.36", (api) => { api.modifyClass("component:d-navigation", { pluginId: "custom-wizard", actions: { @@ -35,6 +36,50 @@ export default { }, }, }); + + api.modifyClass("component:uppy-image-uploader", { + pluginId: "custom-wizard", + // Needed to ensure appEvents get registered when navigating between steps + @observes("id") + initOnStepChange() { + if (/wizard-field|wizard-step/.test(this.id)) { + this._initialize(); + } + }, + }); + + api.modifyClass("component:d-editor", { + pluginId: "custom-wizard", + + didInsertElement() { + this._super(...arguments); + + if (this.wizardComposer) { + this.appEvents.on( + `wizard-editor:insert-text`, + this, + "_wizardInsertText" + ); + this.appEvents.on( + "wizard-editor:replace-text", + this, + "_wizardReplaceText" + ); + } + }, + + _wizardInsertText(text, options) { + if (this.session.wizardEventFieldId === this.fieldId) { + this.insertText(text, options); + } + }, + + _wizardReplaceText(oldVal, newVal, opts = {}) { + if (this.session.wizardEventFieldId === this.fieldId) { + this.replaceText(oldVal, newVal, opts); + } + }, + }); }); }, }; diff --git a/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 index e413cd17..5667bdf8 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 @@ -5,8 +5,8 @@ export default { after: "message-bus", initialize: function (container) { - const messageBus = container.lookup("message-bus:main"); - const siteSettings = container.lookup("site-settings:main"); + const messageBus = container.lookup("service:message-bus"); + const siteSettings = container.lookup("service:site-settings"); if (!siteSettings.custom_wizard_enabled || !messageBus) { return; diff --git a/assets/javascripts/discourse/lib/wizard-json.js.es6 b/assets/javascripts/discourse/lib/wizard-json.js.es6 index 79da60cb..95eaba49 100644 --- a/assets/javascripts/discourse/lib/wizard-json.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-json.js.es6 @@ -97,11 +97,6 @@ function buildObjectArray(json, type) { if (present(json)) { json.forEach((objJson, objectIndex) => { let object = buildObject(objJson, type, objectIndex); - - if (hasAdvancedProperties(object, type)) { - object.set("showAdvanced", true); - } - array.pushObject(object); }); } @@ -112,21 +107,11 @@ function buildObjectArray(json, type) { function buildBasicProperties(json, type, props, objectIndex = null) { listProperties(type).forEach((p) => { props[p] = buildProperty(json, p, type, objectIndex); - - if (hasAdvancedProperties(json, type)) { - props.showAdvanced = true; - } }); return props; } -function hasAdvancedProperties(object, type) { - return Object.keys(object).some((p) => { - return wizardSchema[type].advanced.indexOf(p) > -1 && present(object[p]); - }); -} - /// to be removed: necessary due to action array being moved from step to wizard function actionPatch(json) { let actions = json.actions || []; diff --git a/assets/javascripts/discourse/lib/wizard-schema.js.es6 b/assets/javascripts/discourse/lib/wizard-schema.js.es6 index 5445223a..8c3323ff 100644 --- a/assets/javascripts/discourse/lib/wizard-schema.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-schema.js.es6 @@ -19,7 +19,6 @@ const wizard = { permitted: null, }, mapped: ["permitted"], - advanced: ["restart_on_revisit"], required: ["id"], dependent: { after_time: "after_time_scheduled", @@ -41,8 +40,8 @@ const step = { id: null, index: null, title: null, - key: null, banner: null, + banner_upload_id: null, raw_description: null, required_data: null, required_data_message: null, @@ -51,7 +50,6 @@ const step = { force_final: false, }, mapped: ["required_data", "permitted_params", "condition", "index"], - advanced: ["required_data", "permitted_params", "condition", "index"], required: ["id"], dependent: {}, objectArrays: { @@ -68,15 +66,15 @@ const field = { index: null, label: null, image: null, + image_upload_id: null, description: null, + property: null, required: null, - key: null, type: null, condition: null, }, types: {}, mapped: ["prefill", "content", "condition", "index"], - advanced: ["property", "key", "condition", "index"], required: ["id", "type"], dependent: {}, objectArrays: {}, @@ -100,6 +98,8 @@ const action = { custom_fields: null, skip_redirect: null, suppress_notifications: null, + add_event: null, + add_location: null, }, send_message: { title: null, @@ -132,6 +132,12 @@ const action = { wizard_user: true, usernames: null, }, + watch_tags: { + tags: null, + notification_level: null, + wizard_user: true, + usernames: null, + }, send_to_api: { api: null, api_endpoint: null, @@ -196,23 +202,27 @@ const action = { "messageable_level", "visibility_level", "members_visibility_level", - ], - advanced: [ - "code", - "custom_fields", - "skip_redirect", - "suppress_notifications", - "required", + "add_event", + "add_location", ], required: ["id", "type"], dependent: {}, objectArrays: {}, }; +const custom_field = { + klass: ["topic", "post", "group", "category"], + type: ["string", "boolean", "integer", "json"], +}; + +field.type = Object.keys(field.types); +action.type = Object.keys(action.types); + const wizardSchema = { wizard, step, field, + custom_field, action, }; @@ -224,7 +234,7 @@ export function buildFieldValidations(validations) { wizardSchema.field.validations = validations; } -const siteSettings = getOwner(this).lookup("site-settings:main"); +const siteSettings = getOwner(this).lookup("service:site-settings"); if (siteSettings.wizard_apis_enabled) { wizardSchema.action.types.send_to_api = { api: null, diff --git a/assets/javascripts/discourse/lib/wizard-submission.js.es6 b/assets/javascripts/discourse/lib/wizard-submission.js.es6 new file mode 100644 index 00000000..8fa2a3be --- /dev/null +++ b/assets/javascripts/discourse/lib/wizard-submission.js.es6 @@ -0,0 +1,39 @@ +import EmberObject from "@ember/object"; + +function formatModel(model) { + let fields = [ + EmberObject.create({ + id: "submitted_at", + label: "Submitted At", + enabled: true, + }), + EmberObject.create({ id: "username", label: "User", enabled: true }), + ]; + let submissions = []; + + model.submissions.forEach((s) => { + let submission = { + submitted_at: s.submitted_at, + username: s.user, + }; + + Object.keys(s.fields).forEach((fieldId) => { + if (!fields.some((field) => field.id === fieldId)) { + fields.push( + EmberObject.create({ + id: fieldId, + label: s.fields[fieldId].label, + enabled: true, + }) + ); + } + submission[fieldId] = s.fields[fieldId]; + }); + + submissions.push(EmberObject.create(submission)); + }); + + return { fields, submissions }; +} + +export { formatModel }; diff --git a/assets/javascripts/discourse/lib/wizard.js.es6 b/assets/javascripts/discourse/lib/wizard.js.es6 index 98bdbfdd..2eaccb17 100644 --- a/assets/javascripts/discourse/lib/wizard.js.es6 +++ b/assets/javascripts/discourse/lib/wizard.js.es6 @@ -1,5 +1,6 @@ import EmberObject from "@ember/object"; import wizardSchema from "./wizard-schema"; +import I18n from "I18n"; function selectKitContent(content) { return content.map((i) => ({ id: i, name: i })); @@ -33,6 +34,10 @@ function camelCase(string) { }); } +function translationOrText(i18nKey, text) { + return I18n.findTranslation(i18nKey) ? I18n.t(i18nKey) : text; +} + const userProperties = [ "name", "username", @@ -121,4 +126,5 @@ export { notificationLevels, wizardFieldList, sentenceCase, + translationOrText, }; diff --git a/assets/javascripts/discourse/mixins/subscription.js.es6 b/assets/javascripts/discourse/mixins/subscription.js.es6 new file mode 100644 index 00000000..34276c3f --- /dev/null +++ b/assets/javascripts/discourse/mixins/subscription.js.es6 @@ -0,0 +1,53 @@ +import Mixin from "@ember/object/mixin"; +import { getOwner } from "discourse-common/lib/get-owner"; +import { readOnly } from "@ember/object/computed"; +import discourseComputed from "discourse-common/utils/decorators"; + +const PRODUCT_PAGE = "https://custom-wizard.pavilion.tech"; +const SUPPORT_MESSAGE = + "https://coop.pavilion.tech/new-message?username=support&title=Custom%20Wizard%20Support"; +const MANAGER_CATEGORY = + "https://discourse.pluginmanager.org/c/discourse-custom-wizard"; + +export default Mixin.create({ + subscriptionLandingUrl: PRODUCT_PAGE, + subscriptionClientUrl: "/admin/plugins/subscription-client", + + @discourseComputed + adminWizards() { + return getOwner(this).lookup("controller:admin-wizards"); + }, + + subscribed: readOnly("adminWizards.subscribed"), + subscriptionType: readOnly("adminWizards.subscriptionType"), + businessSubscription: readOnly("adminWizards.businessSubscription"), + communitySubscription: readOnly("adminWizards.communitySubscription"), + standardSubscription: readOnly("adminWizards.standardSubscription"), + subscriptionAttributes: readOnly("adminWizards.subscriptionAttributes"), + subscriptionClientInstalled: readOnly( + "adminWizards.subscriptionClientInstalled" + ), + + @discourseComputed("subscriptionClientInstalled") + subscriptionLink(subscriptionClientInstalled) { + return subscriptionClientInstalled + ? this.subscriptionClientUrl + : this.subscriptionLandingUrl; + }, + + @discourseComputed("subscriptionType") + subscriptionCtaLink(subscriptionType) { + switch (subscriptionType) { + case "none": + return PRODUCT_PAGE; + case "standard": + return SUPPORT_MESSAGE; + case "business": + return SUPPORT_MESSAGE; + case "community": + return MANAGER_CATEGORY; + default: + return PRODUCT_PAGE; + } + }, +}); diff --git a/assets/javascripts/discourse/mixins/valid-state.js.es6 b/assets/javascripts/discourse/mixins/valid-state.js.es6 new file mode 100644 index 00000000..ca86d7e4 --- /dev/null +++ b/assets/javascripts/discourse/mixins/valid-state.js.es6 @@ -0,0 +1,36 @@ +import discourseComputed from "discourse-common/utils/decorators"; + +export const States = { + UNCHECKED: 0, + INVALID: 1, + VALID: 2, +}; + +export default { + _validState: null, + errorDescription: null, + + init() { + this._super(...arguments); + this.set("_validState", States.UNCHECKED); + }, + + @discourseComputed("_validState") + valid: (state) => state === States.VALID, + + @discourseComputed("_validState") + invalid: (state) => state === States.INVALID, + + @discourseComputed("_validState") + unchecked: (state) => state === States.UNCHECKED, + + setValid(valid, description) { + this.set("_validState", valid ? States.VALID : States.INVALID); + + if (!valid && description && description.length) { + this.set("errorDescription", description); + } else { + this.set("errorDescription", null); + } + }, +}; diff --git a/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 b/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 new file mode 100644 index 00000000..65c7aa7f --- /dev/null +++ b/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 @@ -0,0 +1,230 @@ +import EmberObject from "@ember/object"; +import { buildProperties, mapped, present } from "../lib/wizard-json"; +import { listProperties, snakeCase } from "../lib/wizard"; +import wizardSchema from "../lib/wizard-schema"; +import { Promise } from "rsvp"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const CustomWizardAdmin = EmberObject.extend({ + save(opts) { + return new Promise((resolve, reject) => { + let wizard = this.buildJson(this, "wizard"); + + if (wizard.error) { + reject(wizard); + } + + let data = { + wizard, + }; + + if (opts.create) { + data.create = true; + } + + ajax(`/admin/wizards/wizard/${wizard.id}`, { + type: "PUT", + contentType: "application/json", + data: JSON.stringify(data), + }).then((result) => { + if (result.backend_validation_error) { + reject(result); + } else { + resolve(result); + } + }); + }); + }, + + buildJson(object, type, result = {}) { + let objectType = object.type || null; + + if (wizardSchema[type].types) { + if (!objectType) { + result.error = { + type: "required", + params: { type, property: "type" }, + }; + return result; + } + } + + for (let property of listProperties(type, { objectType })) { + let value = object.get(property); + + result = this.validateValue(property, value, object, type, result); + + if (result.error) { + break; + } + + if (mapped(property, type)) { + value = this.buildMappedJson(value); + } + + if (value !== undefined && value !== null) { + result[property] = value; + } + } + + if (!result.error) { + for (let arrayObjectType of Object.keys( + wizardSchema[type].objectArrays + )) { + let arraySchema = wizardSchema[type].objectArrays[arrayObjectType]; + let objectArray = object.get(arraySchema.property); + + if (arraySchema.required && !present(objectArray)) { + result.error = { + type: "required", + params: { type, property: arraySchema.property }, + }; + break; + } + + result[arraySchema.property] = []; + + for (let item of objectArray) { + let itemProps = this.buildJson(item, arrayObjectType); + + if (itemProps.error) { + result.error = itemProps.error; + break; + } else { + result[arraySchema.property].push(itemProps); + } + } + } + } + + return result; + }, + + validateValue(property, value, object, type, result) { + if (wizardSchema[type].required.indexOf(property) > -1 && !value) { + result.error = { + type: "required", + params: { type, property }, + }; + } + + let dependent = wizardSchema[type].dependent[property]; + if (dependent && value && !object[dependent]) { + result.error = { + type: "dependent", + params: { property, dependent }, + }; + } + + if (property === "api_body") { + try { + value = JSON.parse(value); + } catch (e) { + result.error = { + type: "invalid", + params: { type, property }, + }; + } + } + + return result; + }, + + buildMappedJson(value) { + if (typeof value === "string" || Number.isInteger(value)) { + return value; + } + if (!value || !value.length) { + return false; + } + + let inputs = value; + let result = []; + + inputs.forEach((inpt) => { + let input = { + type: inpt.type, + }; + + if (inpt.connector) { + input.connector = inpt.connector; + } + + if (present(inpt.output)) { + input.output = inpt.output; + input.output_type = snakeCase(inpt.output_type); + input.output_connector = inpt.output_connector; + } + + if (present(inpt.pairs)) { + input.pairs = []; + + inpt.pairs.forEach((pr) => { + if (present(pr.key) && present(pr.value)) { + let pairParams = { + index: pr.index, + key: pr.key, + key_type: snakeCase(pr.key_type), + value: pr.value, + value_type: snakeCase(pr.value_type), + connector: pr.connector, + }; + + input.pairs.push(pairParams); + } + }); + } + + if ( + (input.type === "assignment" && present(input.output)) || + present(input.pairs) + ) { + result.push(input); + } + }); + + if (!result.length) { + result = false; + } + + return result; + }, + + remove() { + return ajax(`/admin/wizards/wizard/${this.id}`, { + type: "DELETE", + }) + .then(() => this.destroy()) + .catch(popupAjaxError); + }, +}); + +CustomWizardAdmin.reopenClass({ + all() { + return ajax("/admin/wizards/wizard", { + type: "GET", + }) + .then((result) => { + return result.wizard_list; + }) + .catch(popupAjaxError); + }, + + submissions(wizardId, page = 0) { + return ajax(`/admin/wizards/submissions/${wizardId}`, { + type: "GET", + data: { + page, + }, + }).catch(popupAjaxError); + }, + + create(wizardJson = {}) { + const wizard = this._super.apply(this); + wizard.setProperties(buildProperties(wizardJson)); + return wizard; + }, +}); + +export default CustomWizardAdmin; diff --git a/assets/javascripts/discourse/models/custom-wizard-field.js.es6 b/assets/javascripts/discourse/models/custom-wizard-field.js.es6 new file mode 100644 index 00000000..a03c7c9e --- /dev/null +++ b/assets/javascripts/discourse/models/custom-wizard-field.js.es6 @@ -0,0 +1,79 @@ +import EmberObject from "@ember/object"; +import ValidState from "discourse/plugins/discourse-custom-wizard/discourse/mixins/valid-state"; +import discourseComputed from "discourse-common/utils/decorators"; +import { translationOrText } from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard"; + +const StandardFieldValidation = [ + "text", + "number", + "textarea", + "dropdown", + "tag", + "image", + "user_selector", + "text_only", + "composer", + "category", + "group", + "date", + "time", + "date_time", +]; + +export default EmberObject.extend(ValidState, { + id: null, + type: null, + value: null, + required: null, + warning: null, + + @discourseComputed("wizardId", "stepId", "id") + i18nKey(wizardId, stepId, id) { + return `${wizardId}.${stepId}.${id}`; + }, + + @discourseComputed("i18nKey", "label") + translatedLabel(i18nKey, label) { + return translationOrText(`${i18nKey}.label`, label); + }, + + @discourseComputed("i18nKey", "placeholder") + translatedPlaceholder(i18nKey, placeholder) { + return translationOrText(`${i18nKey}.placeholder`, placeholder); + }, + + @discourseComputed("i18nKey", "description") + translatedDescription(i18nKey, description) { + return translationOrText(`${i18nKey}.description`, description); + }, + + check() { + if (this.customCheck) { + return this.customCheck(); + } + + let valid = this.valid; + + if (!this.required) { + this.setValid(true); + return true; + } + + const val = this.get("value"); + const type = this.get("type"); + + if (type === "checkbox") { + valid = val; + } else if (type === "upload") { + valid = val && val.id > 0; + } else if (StandardFieldValidation.indexOf(type) > -1) { + valid = val && val.toString().length > 0; + } else if (type === "url") { + valid = true; + } + + this.setValid(valid); + + return valid; + }, +}); diff --git a/assets/javascripts/discourse/models/custom-wizard-logs.js.es6 b/assets/javascripts/discourse/models/custom-wizard-logs.js.es6 index e2de8a07..23565e2c 100644 --- a/assets/javascripts/discourse/models/custom-wizard-logs.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-logs.js.es6 @@ -3,14 +3,54 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import EmberObject from "@ember/object"; const CustomWizardLogs = EmberObject.extend(); +const logItemTypes = { + date: "date_time", + action: "text", + message: "long_text", + user: "user", + username: "text", +}; + +function logItem(item, attr) { + return { + value: item[attr], + type: logItemTypes[attr], + }; +} CustomWizardLogs.reopenClass({ - list(page = 0) { - return ajax("/admin/wizards/logs", { - data: { - page, - }, - }).catch(popupAjaxError); + list(wizardId, page = 0) { + let data = { + page, + }; + + return ajax(`/admin/wizards/logs/${wizardId}`, { data }) + .catch(popupAjaxError) + .then((result) => { + if (result.logs) { + result.logs = result.logs.map((item) => { + let map = {}; + + if (item.date) { + map.date = logItem(item, "date"); + } + if (item.action) { + map.action = logItem(item, "action"); + } + if (item.user) { + map.user = item.user; + } else { + map.user = logItem(item, "username"); + } + if (item.message) { + map.message = logItem(item, "message"); + } + + return map; + }); + } + return result; + }); }, }); diff --git a/assets/javascripts/discourse/models/custom-wizard-step.js.es6 b/assets/javascripts/discourse/models/custom-wizard-step.js.es6 new file mode 100644 index 00000000..f7cdc497 --- /dev/null +++ b/assets/javascripts/discourse/models/custom-wizard-step.js.es6 @@ -0,0 +1,114 @@ +import EmberObject from "@ember/object"; +import ValidState from "discourse/plugins/discourse-custom-wizard/discourse/mixins/valid-state"; +import { ajax } from "discourse/lib/ajax"; +import discourseComputed from "discourse-common/utils/decorators"; +import { later } from "@ember/runloop"; +import { translationOrText } from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard"; + +export default EmberObject.extend(ValidState, { + id: null, + + @discourseComputed("wizardId", "id") + i18nKey(wizardId, stepId) { + return `${wizardId}.${stepId}`; + }, + + @discourseComputed("i18nKey", "title") + translatedTitle(i18nKey, title) { + return translationOrText(`${i18nKey}.title`, title); + }, + + @discourseComputed("i18nKey", "description") + translatedDescription(i18nKey, description) { + return translationOrText(`${i18nKey}.description`, description); + }, + + @discourseComputed("index") + displayIndex: (index) => index + 1, + + @discourseComputed("fields.[]") + fieldsById(fields) { + const lookup = {}; + fields.forEach((field) => (lookup[field.get("id")] = field)); + return lookup; + }, + + validate() { + let allValid = true; + const result = { warnings: [] }; + + this.fields.forEach((field) => { + allValid = allValid && field.check(); + const warning = field.get("warning"); + if (warning) { + result.warnings.push(warning); + } + }); + + this.setValid(allValid); + + return result; + }, + + fieldError(id, description) { + const field = this.fields.findBy("id", id); + if (field) { + field.setValid(false, description); + } + }, + + save() { + const wizardId = this.get("wizardId"); + const fields = {}; + + this.get("fields").forEach((f) => { + if (f.type !== "text_only") { + fields[f.id] = f.value; + } + }); + + return ajax({ + url: `/w/${wizardId}/steps/${this.get("id")}`, + type: "PUT", + data: { fields }, + }).catch((response) => { + if (response.jqXHR) { + response = response.jqXHR; + } + if (response && response.responseJSON && response.responseJSON.errors) { + let wizardErrors = []; + response.responseJSON.errors.forEach((err) => { + if (err.field === wizardId) { + wizardErrors.push(err.description); + } else if (err.field) { + this.fieldError(err.field, err.description); + } else if (err) { + wizardErrors.push(err); + } + }); + if (wizardErrors.length) { + this.handleWizardError(wizardErrors.join("\n")); + } + this.animateInvalidFields(); + throw response; + } + + if (response && response.responseText) { + const responseText = response.responseText; + const start = responseText.indexOf(">") + 1; + const end = responseText.indexOf("plugins"); + const message = responseText.substring(start, end); + this.handleWizardError(message); + throw message; + } + }); + }, + + handleWizardError(message) { + this.set("message", { + state: "error", + text: message, + }); + later(() => this.set("message", null), 6000); + }, +}); diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 2fa6bc4c..77f439c7 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -1,227 +1,127 @@ -import { ajax } from "discourse/lib/ajax"; import EmberObject from "@ember/object"; -import { buildProperties, mapped, present } from "../lib/wizard-json"; -import { listProperties, snakeCase } from "../lib/wizard"; -import wizardSchema from "../lib/wizard-schema"; -import { Promise } from "rsvp"; +import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import discourseComputed from "discourse-common/utils/decorators"; +import getUrl from "discourse-common/lib/get-url"; +import CustomWizardField from "./custom-wizard-field"; +import CustomWizardStep from "./custom-wizard-step"; const CustomWizard = EmberObject.extend({ - save(opts) { - return new Promise((resolve, reject) => { - let wizard = this.buildJson(this, "wizard"); + @discourseComputed("steps.length") + totalSteps: (length) => length, - if (wizard.error) { - reject(wizard); - } - - let data = { - wizard, - }; - - if (opts.create) { - data.create = true; - } - - ajax(`/admin/wizards/wizard/${wizard.id}`, { - type: "PUT", - contentType: "application/json", - data: JSON.stringify(data), - }).then((result) => { - if (result.backend_validation_error) { - reject(result); - } else { - resolve(result); - } - }); - }); + skip() { + if (this.required && !this.completed && this.permitted) { + return; + } + CustomWizard.skip(this.id); }, - buildJson(object, type, result = {}) { - let objectType = object.type || null; - - if (wizardSchema[type].types) { - if (!objectType) { - result.error = { - type: "required", - params: { type, property: "type" }, - }; - return result; - } - } - - for (let property of listProperties(type, { objectType })) { - let value = object.get(property); - - result = this.validateValue(property, value, object, type, result); - - if (result.error) { - break; - } - - if (mapped(property, type)) { - value = this.buildMappedJson(value); - } - - if (value !== undefined && value !== null) { - result[property] = value; - } - } - - if (!result.error) { - for (let arrayObjectType of Object.keys( - wizardSchema[type].objectArrays - )) { - let arraySchema = wizardSchema[type].objectArrays[arrayObjectType]; - let objectArray = object.get(arraySchema.property); - - if (arraySchema.required && !present(objectArray)) { - result.error = { - type: "required", - params: { type, property: arraySchema.property }, - }; - break; - } - - result[arraySchema.property] = []; - - for (let item of objectArray) { - let itemProps = this.buildJson(item, arrayObjectType); - - if (itemProps.error) { - result.error = itemProps.error; - break; - } else { - result[arraySchema.property].push(itemProps); - } - } - } - } - - return result; - }, - - validateValue(property, value, object, type, result) { - if (wizardSchema[type].required.indexOf(property) > -1 && !value) { - result.error = { - type: "required", - params: { type, property }, - }; - } - - let dependent = wizardSchema[type].dependent[property]; - if (dependent && value && !object[dependent]) { - result.error = { - type: "dependent", - params: { property, dependent }, - }; - } - - if (property === "api_body") { - try { - value = JSON.parse(value); - } catch (e) { - result.error = { - type: "invalid", - params: { type, property }, - }; - } - } - - return result; - }, - - buildMappedJson(value) { - if (typeof value === "string" || Number.isInteger(value)) { - return value; - } - if (!value || !value.length) { - return false; - } - - let inputs = value; - let result = []; - - inputs.forEach((inpt) => { - let input = { - type: inpt.type, - }; - - if (inpt.connector) { - input.connector = inpt.connector; - } - - if (present(inpt.output)) { - input.output = inpt.output; - input.output_type = snakeCase(inpt.output_type); - input.output_connector = inpt.output_connector; - } - - if (present(inpt.pairs)) { - input.pairs = []; - - inpt.pairs.forEach((pr) => { - if (present(pr.key) && present(pr.value)) { - let pairParams = { - index: pr.index, - key: pr.key, - key_type: snakeCase(pr.key_type), - value: pr.value, - value_type: snakeCase(pr.value_type), - connector: pr.connector, - }; - - input.pairs.push(pairParams); - } - }); - } - - if ( - (input.type === "assignment" && present(input.output)) || - present(input.pairs) - ) { - result.push(input); - } - }); - - if (!result.length) { - result = false; - } - - return result; - }, - - remove() { - return ajax(`/admin/wizards/wizard/${this.id}`, { - type: "DELETE", - }) - .then(() => this.destroy()) - .catch(popupAjaxError); + restart() { + CustomWizard.restart(this.id); }, }); CustomWizard.reopenClass({ - all() { - return ajax("/admin/wizards/wizard", { - type: "GET", - }) + skip(wizardId) { + ajax({ url: `/w/${wizardId}/skip`, type: "PUT" }) .then((result) => { - return result.wizard_list; + CustomWizard.finished(result); }) .catch(popupAjaxError); }, - submissions(wizardId) { - return ajax(`/admin/wizards/submissions/${wizardId}`, { - type: "GET", - }).catch(popupAjaxError); + restart(wizardId) { + ajax({ url: `/w/${wizardId}/skip`, type: "PUT" }) + .then(() => { + window.location.href = `/w/${wizardId}`; + }) + .catch(popupAjaxError); }, - create(wizardJson = {}) { - const wizard = this._super.apply(this); - wizard.setProperties(buildProperties(wizardJson)); - return wizard; + finished(result) { + let url = "/"; + if (result.redirect_on_complete) { + url = result.redirect_on_complete; + } + window.location.href = getUrl(url); + }, + + build(wizardJson) { + if (!wizardJson) { + return null; + } + + if (!wizardJson.completed && wizardJson.steps) { + wizardJson.steps = wizardJson.steps + .map((step) => { + const stepObj = CustomWizardStep.create(step); + stepObj.wizardId = wizardJson.id; + + stepObj.fields.sort((a, b) => { + return parseFloat(a.number) - parseFloat(b.number); + }); + + let tabindex = 1; + stepObj.fields.forEach((f) => { + f.tabindex = tabindex; + + if (["date_time"].includes(f.type)) { + tabindex = tabindex + 2; + } else { + tabindex++; + } + }); + + stepObj.fields = stepObj.fields.map((f) => { + f.wizardId = wizardJson.id; + f.stepId = stepObj.id; + return CustomWizardField.create(f); + }); + + return stepObj; + }) + .sort((a, b) => { + return parseFloat(a.index) - parseFloat(b.index); + }); + } + return CustomWizard.create(wizardJson); }, }); +export function findCustomWizard(wizardId, params = {}) { + let url = `/w/${wizardId}.json`; + + let paramKeys = Object.keys(params).filter((k) => { + if (k === "wizard_id") { + return false; + } + return !!params[k]; + }); + + if (paramKeys.length) { + url += "?"; + paramKeys.forEach((k, i) => { + if (i > 0) { + url += "&"; + } + url += `${k}=${params[k]}`; + }); + } + + return ajax(url).then((result) => { + return CustomWizard.build(result); + }); +} + +let _wizard_store; + +export function updateCachedWizard(wizard) { + _wizard_store = wizard; +} + +export function getCachedWizard() { + return _wizard_store; +} + export default CustomWizard; diff --git a/assets/javascripts/discourse/routes/admin-wizards-custom-fields.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-custom-fields.js.es6 index a1c625ad..a04d36f9 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-custom-fields.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-custom-fields.js.es6 @@ -8,7 +8,10 @@ export default DiscourseRoute.extend({ }, setupController(controller, model) { - const customFields = A(model || []); - controller.set("customFields", customFields); + const customFields = A(model.custom_fields || []); + + controller.setProperties({ + customFields, + }); }, }); diff --git a/assets/javascripts/discourse/routes/admin-wizards-logs-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-logs-show.js.es6 new file mode 100644 index 00000000..474360ec --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-wizards-logs-show.js.es6 @@ -0,0 +1,17 @@ +import CustomWizardLogs from "../models/custom-wizard-logs"; +import DiscourseRoute from "discourse/routes/discourse"; +import { A } from "@ember/array"; + +export default DiscourseRoute.extend({ + model(params) { + return CustomWizardLogs.list(params.wizardId); + }, + + setupController(controller, model) { + controller.setProperties({ + wizard: model.wizard, + logs: A(model.logs), + total: model.total, + }); + }, +}); diff --git a/assets/javascripts/discourse/routes/admin-wizards-logs.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-logs.js.es6 index 56b91350..a1575050 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-logs.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-logs.js.es6 @@ -1,12 +1,24 @@ -import CustomWizardLogs from "../models/custom-wizard-logs"; import DiscourseRoute from "discourse/routes/discourse"; +import { ajax } from "discourse/lib/ajax"; export default DiscourseRoute.extend({ model() { - return CustomWizardLogs.list(); + return ajax(`/admin/wizards/wizard`); }, setupController(controller, model) { - controller.set("logs", model); + const showParams = this.paramsFor("adminWizardsLogsShow"); + + controller.setProperties({ + wizardId: showParams.wizardId, + wizardList: model.wizard_list, + }); + }, + + actions: { + changeWizard(wizardId) { + this.controllerFor("adminWizardsLogs").set("wizardId", wizardId); + this.transitionTo("adminWizardsLogsShow", wizardId); + }, }, }); diff --git a/assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 index dfbfc472..b3314186 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-manager.js.es6 @@ -1,9 +1,9 @@ -import CustomWizard from "../models/custom-wizard"; +import CustomWizardAdmin from "../models/custom-wizard-admin"; import DiscourseRoute from "discourse/routes/discourse"; export default DiscourseRoute.extend({ model() { - return CustomWizard.all(); + return CustomWizardAdmin.all(); }, setupController(controller, model) { diff --git a/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 index 73168ff3..b616b5be 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-submissions-show.js.es6 @@ -1,42 +1,21 @@ -import CustomWizard from "../models/custom-wizard"; +import { A } from "@ember/array"; +import CustomWizardAdmin from "../models/custom-wizard-admin"; import DiscourseRoute from "discourse/routes/discourse"; - -const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"]; +import { formatModel } from "../lib/wizard-submission"; export default DiscourseRoute.extend({ model(params) { - return CustomWizard.submissions(params.wizardId); + return CustomWizardAdmin.submissions(params.wizardId); }, setupController(controller, model) { - if (model && model.submissions) { - let fields = ["username"]; - model.submissions.forEach((s) => { - Object.keys(s.fields).forEach((k) => { - if (!excludedMetaFields.includes(k) && fields.indexOf(k) < 0) { - fields.push(k); - } - }); - }); + const { fields, submissions } = formatModel(model); - let submissions = []; - model.submissions.forEach((s) => { - let submission = { - username: s.username, - }; - Object.keys(s.fields).forEach((f) => { - if (fields.includes(f)) { - submission[f] = s.fields[f]; - } - }); - submissions.push(submission); - }); - - controller.setProperties({ - wizard: model.wizard, - submissions, - fields, - }); - } + controller.setProperties({ + wizard: model.wizard, + fields: A(fields), + submissions: A(submissions), + total: model.total, + }); }, }); diff --git a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 index cb2d54c3..f55ff19e 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 @@ -1,4 +1,4 @@ -import CustomWizard from "../models/custom-wizard"; +import CustomWizardAdmin from "../models/custom-wizard-admin"; import { ajax } from "discourse/lib/ajax"; import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; @@ -20,7 +20,9 @@ export default DiscourseRoute.extend({ setupController(controller, model) { const parentModel = this.modelFor("adminWizardsWizard"); - const wizard = CustomWizard.create(!model || model.create ? {} : model); + const wizard = CustomWizardAdmin.create( + !model || model.create ? {} : model + ); const fieldTypes = Object.keys(parentModel.field_types).map((type) => { return { id: type, diff --git a/assets/javascripts/discourse/routes/admin-wizards.js.es6 b/assets/javascripts/discourse/routes/admin-wizards.js.es6 index 5de271a8..1fa786d3 100644 --- a/assets/javascripts/discourse/routes/admin-wizards.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards.js.es6 @@ -1,7 +1,21 @@ import DiscourseRoute from "discourse/routes/discourse"; +import { ajax } from "discourse/lib/ajax"; export default DiscourseRoute.extend({ - beforeModel(transition) { + model() { + return ajax("/admin/wizards"); + }, + + setupController(controller, model) { + controller.setProperties({ + subscribed: model.subscribed, + subscriptionType: model.subscription_type, + subscriptionAttributes: model.subscription_attributes, + subscriptionClientInstalled: model.subscription_client_installed, + }); + }, + + afterModel(model, transition) { if (transition.targetName === "adminWizards.index") { this.transitionTo("adminWizardsWizard"); } diff --git a/assets/javascripts/wizard/routes/custom-index.js.es6 b/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 similarity index 51% rename from assets/javascripts/wizard/routes/custom-index.js.es6 rename to assets/javascripts/discourse/routes/custom-wizard-index.js.es6 index a8abc152..1d5a71c7 100644 --- a/assets/javascripts/wizard/routes/custom-index.js.es6 +++ b/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 @@ -1,10 +1,17 @@ -import { getCachedWizard } from "../models/custom"; +import { getCachedWizard } from "../models/custom-wizard"; +import Route from "@ember/routing/route"; -export default Ember.Route.extend({ +export default Route.extend({ beforeModel() { const wizard = getCachedWizard(); - if (wizard && wizard.permitted && !wizard.completed && wizard.start) { - this.replaceWith("custom.step", wizard.start); + if ( + wizard && + wizard.user && + wizard.permitted && + !wizard.completed && + wizard.start + ) { + this.replaceWith("customWizardStep", wizard.start); } }, @@ -19,15 +26,18 @@ export default Ember.Route.extend({ const wizardId = model.get("id"); const user = model.get("user"); const name = model.get("name"); + const requiresLogin = !user; + const notPermitted = !permitted; - controller.setProperties({ - requiresLogin: !user, + const props = { + requiresLogin, user, name, completed, - notPermitted: !permitted, + notPermitted, wizardId, - }); + }; + controller.setProperties(props); } else { controller.set("noWizard", true); } diff --git a/assets/javascripts/wizard/routes/custom-step.js.es6 b/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 similarity index 61% rename from assets/javascripts/wizard/routes/custom-step.js.es6 rename to assets/javascripts/discourse/routes/custom-wizard-step.js.es6 index 8088727a..969df1eb 100644 --- a/assets/javascripts/wizard/routes/custom-step.js.es6 +++ b/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 @@ -1,9 +1,15 @@ -import WizardI18n from "../lib/wizard-i18n"; -import { getCachedWizard } from "../models/custom"; +import I18n from "I18n"; +import { getCachedWizard } from "../models/custom-wizard"; +import Route from "@ember/routing/route"; -export default Ember.Route.extend({ +export default Route.extend({ beforeModel() { - this.set("wizard", getCachedWizard()); + const wizard = getCachedWizard(); + this.set("wizard", wizard); + + if (!wizard || !wizard.user || !wizard.permitted || wizard.completed) { + this.replaceWith("customWizard"); + } }, model(params) { @@ -19,7 +25,7 @@ export default Ember.Route.extend({ afterModel(model) { if (model.completed) { - return this.transitionTo("index"); + return this.transitionTo("wizard.index"); } return model.set("wizardId", this.wizard.id); }, @@ -33,8 +39,7 @@ export default Ember.Route.extend({ if (!model.permitted) { props["stepMessage"] = { state: "not-permitted", - text: - model.permitted_message || WizardI18n("wizard.step_not_permitted"), + text: model.permitted_message || I18n.t("wizard.step_not_permitted"), }; if (model.index > 0) { props["showReset"] = true; diff --git a/assets/javascripts/discourse/routes/custom-wizard.js.es6 b/assets/javascripts/discourse/routes/custom-wizard.js.es6 new file mode 100644 index 00000000..d03714a2 --- /dev/null +++ b/assets/javascripts/discourse/routes/custom-wizard.js.es6 @@ -0,0 +1,89 @@ +import { findCustomWizard, updateCachedWizard } from "../models/custom-wizard"; +import I18n from "I18n"; +import DiscourseRoute from "discourse/routes/discourse"; + +export default DiscourseRoute.extend({ + titleToken() { + const wizard = this.modelFor("custom-wizard"); + return wizard ? wizard.name || wizard.id : I18n.t("wizard.custom_title"); + }, + + beforeModel(transition) { + if (transition.intent.queryParams) { + this.set("queryParams", transition.intent.queryParams); + } + }, + + model(params) { + return findCustomWizard(params.wizard_id, this.get("queryParams")); + }, + + showDialog(wizardModel) { + const title = I18n.t("wizard.incomplete_submission.title", { + date: moment(wizardModel.submission_last_updated_at).format( + "MMMM Do YYYY" + ), + }); + + const buttons = [ + { + label: I18n.t("wizard.incomplete_submission.restart"), + class: "btn btn-default", + callback: () => { + wizardModel.restart(); + }, + }, + { + label: I18n.t("wizard.incomplete_submission.resume"), + class: "btn btn-primary", + }, + ]; + + const options = { + onEscape: false, + }; + + bootbox.dialog(title, buttons, options); + }, + + afterModel(model) { + updateCachedWizard(model); + }, + + setupController(controller, model) { + controller.setProperties({ + customWizard: true, + logoUrl: this.siteSettings.logo_small, + reset: null, + model, + }); + + const stepModel = this.modelFor("custom-wizard-step"); + if ( + model.resume_on_revisit && + model.submission_last_updated_at && + stepModel.index > 0 + ) { + this.showDialog(model); + } + + const background = model.get("background"); + if (background) { + document.body.style.background = background; + } + }, + + activate() { + if (!document.body.classList.contains("custom-wizard")) { + document.body.classList.add("custom-wizard"); + } + }, + + deactivate() { + if (document.body.classList.contains("custom-wizard")) { + document.body.classList.remove("custom-wizard"); + } + + document.body.style.background = ""; + }, +}); diff --git a/assets/javascripts/discourse/templates/admin-wizards-api-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-api-show.hbs index 4d3def3d..303b3f6d 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api-show.hbs @@ -21,11 +21,11 @@ {{/if}} -
+
{{#if api.isNew}} {{i18n "admin.wizard.api.new"}} {{else}} - {{api.title}} + {{api.title}} {{/if}}
@@ -35,12 +35,12 @@ {{input value=api.title placeholder=(i18n "admin.wizard.api.title_placeholder")}}
-
+
{{#if api.isNew}} {{input value=api.name placeholder=(i18n "admin.wizard.api.name_placeholder")}} {{else}} - {{api.name}} + {{api.name}} {{/if}}
@@ -63,7 +63,7 @@ {{/if}} -
+
{{i18n "admin.wizard.api.auth.label"}}
@@ -71,7 +71,7 @@
-
+
{{i18n "admin.wizard.api.auth.settings"}}
@@ -174,7 +174,7 @@ {{/if}}
-
+
{{i18n "admin.wizard.api.status.label"}}
@@ -220,7 +220,7 @@ {{/if}}
-
+
{{i18n "admin.wizard.api.endpoint.label"}}
@@ -277,11 +277,15 @@ {{/if}}
-
+
{{i18n "admin.wizard.api.log.label"}} - {{d-button action=(action "clearLogs") - icon="trash-alt" - class="clear-logs"}} + +
+ {{d-button + action=(action "clearLogs") + class="clear-logs" + label="admin.wizard.api.log.clear"}} +
@@ -300,7 +304,7 @@ {{logentry.time}} {{logentry.status}} diff --git a/assets/javascripts/discourse/templates/admin-wizards-api.hbs b/assets/javascripts/discourse/templates/admin-wizards-api.hbs index 00d8ad60..af91d0fb 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-api.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-api.hbs @@ -8,7 +8,7 @@ )}} {{d-button - action="createApi" + action=(route-action "createApi") label="admin.wizard.api.create" icon="plus"}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-custom-fields.hbs b/assets/javascripts/discourse/templates/admin-wizards-custom-fields.hbs index 10501498..a90f6299 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-custom-fields.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-custom-fields.hbs @@ -5,7 +5,7 @@ {{d-button label="admin.wizard.custom_field.add" icon="plus" - action="addField"}} + action=(action "addField")}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs-show.hbs new file mode 100644 index 00000000..270d5c21 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin-wizards-logs-show.hbs @@ -0,0 +1,45 @@ +{{#if logs}} +
+ + +
+ {{d-button + label="refresh" + icon="sync" + action=(action "refresh") + class="refresh"}} +
+
+ +
+ {{#load-more selector=".wizard-table tr" action=(action "loadMore")}} + {{#if noResults}} +

{{i18n "search.no_results"}}

+ {{else}} + + + + + + + + + + + {{#each logs as |log|}} + + {{#each-in log as |field value|}} + + {{/each-in}} + + {{/each}} + +
{{i18n "admin.wizard.log.date"}}{{i18n "admin.wizard.log.action"}}{{i18n "admin.wizard.log.user"}}{{i18n "admin.wizard.log.message"}}
{{wizard-table-field field=field value=value}}
+ {{/if}} + + {{conditional-loading-spinner condition=refreshing}} + {{/load-more}} +
+{{/if}} diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs index b0dd3de6..45738a9f 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs @@ -1,34 +1,19 @@ -
-

{{i18n "admin.wizard.log.nav_label"}}

- - {{d-button - label="refresh" - icon="sync" - action="refresh" - class="refresh"}} +
+ {{combo-box + value=wizardId + content=wizardList + onChange=(route-action "changeWizard") + options=(hash + none="admin.wizard.select" + )}}
-{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}} - {{#if noResults}} -

{{i18n "search.no_results"}}

- {{else}} - - - - - - - - - {{#each logs as |log|}} - - - - - {{/each}} - -
MessageDate
{{log.message}}{{bound-date log.date}}
- {{/if}} +{{wizard-message + key=messageKey + opts=messageOpts + url=documentationUrl + component="logs"}} - {{conditional-loading-spinner condition=refreshing}} -{{/load-more}} +
+ {{outlet}} +
diff --git a/assets/javascripts/discourse/templates/admin-wizards-manager.hbs b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs index 9ee2f080..4b91bd3d 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-manager.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-manager.hbs @@ -12,10 +12,10 @@ {{/if}} {{input - id="file-upload" + id="custom-wizard-file-upload" type="file" accept="application/json" - change=(action "setFile")}} + input=(action "setFile")}} {{d-button id="upload-button" label="admin.wizard.manager.upload" diff --git a/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs index 6d1f255b..72ec7c38 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-submissions-show.hbs @@ -1,8 +1,24 @@ {{#if submissions}} -
- - - - {{#each fields as |f|}} - - {{/each}} - - - - {{#each submissions as |s|}} - - {{#each-in s as |k v|}} - - {{/each-in}} - - {{/each}} - -
{{f}}
{{v}}
+
+ {{#load-more selector=".wizard-table tr" action=(action "loadMore")}} + {{#if noResults}} +

{{i18n "search.no_results"}}

+ {{else}} + + + + {{#each fields as |field|}} + {{#if field.enabled}} + + {{/if}} + {{/each}} + + + + {{#each displaySubmissions as |submission|}} + + {{#each-in submission as |field value|}} + + {{/each-in}} + + {{/each}} + +
+ {{field.label}} +
{{wizard-table-field field=field value=value}}
+ {{/if}} + + {{conditional-loading-spinner condition=loadingMore}} + {{/load-more}}
{{/if}} diff --git a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs index d843485a..07dd1682 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-submissions.hbs @@ -1,4 +1,4 @@ -
+
{{combo-box value=wizardId content=wizardList @@ -8,6 +8,12 @@ )}}
+{{wizard-message + key=messageKey + opts=messageOpts + url=documentationUrl + component="submissions"}} +
{{outlet}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs index c5ed70a7..11a2b415 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs @@ -55,21 +55,11 @@
- +
- {{input type="checkbox" checked=wizard.required}} - {{i18n "admin.wizard.required_label"}} -
-
- -
-
- -
-
- {{input type="checkbox" checked=wizard.after_signup}} - {{i18n "admin.wizard.after_signup_label"}} + {{input type="checkbox" checked=wizard.save_submissions}} + {{i18n "admin.wizard.save_submissions_label"}}
@@ -83,6 +73,16 @@
+
+
+ +
+
+ {{input type="checkbox" checked=wizard.after_signup}} + {{i18n "admin.wizard.after_signup_label"}} +
+
+
@@ -101,68 +101,52 @@ {{input type="checkbox" checked=wizard.after_time}} {{i18n "admin.wizard.after_time_label"}} {{d-button - action="setNextSessionScheduled" + action=(action "setNextSessionScheduled") translatedLabel=nextSessionScheduledLabel class="btn-after-time" icon="far-calendar"}}
-
-
- -
-
- {{wizard-mapper - inputs=wizard.permitted - options=(hash - context="wizard" - inputTypes="assignment,validation" - groupSelection="output" - userFieldSelection="key" - textSelection="value" - inputConnector="and" - )}} -
-
- - {{wizard-advanced-toggle showAdvanced=wizard.showAdvanced}} - - {{#if wizard.showAdvanced}} -
- -
-
- -
-
- {{input type="checkbox" checked=wizard.save_submissions}} - {{i18n "admin.wizard.save_submissions_label"}} -
+ {{#wizard-subscription-container}} +
+
+
- -
-
- -
-
- {{input type="checkbox" checked=wizard.restart_on_revisit}} - {{i18n "admin.wizard.restart_on_revisit_label"}} -
+
+ {{input type="checkbox" checked=wizard.required}} + {{i18n "admin.wizard.required_label"}}
- -
-
- -
-
- {{input type="checkbox" checked=wizard.resume_on_revisit}} - {{i18n "admin.wizard.resume_on_revisit_label"}} -
-
-
- {{/if}} + +
+
+ +
+
+ {{input type="checkbox" checked=wizard.restart_on_revisit}} + {{i18n "admin.wizard.restart_on_revisit_label"}} +
+
+ +
+
+ +
+
+ {{wizard-mapper + inputs=wizard.permitted + options=(hash + context="wizard" + inputTypes="assignment,validation" + groupSelection="output" + userFieldSelection="key" + textSelection="value" + inputConnector="and" + )}} +
+
+ {{/wizard-subscription-container}}
{{wizard-links @@ -176,7 +160,8 @@ wizard=wizard currentField=currentField wizardFields=wizardFields - fieldTypes=fieldTypes}} + fieldTypes=fieldTypes + subscribed=subscribed}} {{/if}} {{wizard-links @@ -185,14 +170,15 @@ items=wizard.actions generateLabels=true}} - {{#each wizard.actions as |action|}} + {{#each wizard.actions as |wizardAction|}} {{wizard-custom-action - action=action + action=wizardAction currentActionId=currentAction.id wizard=wizard apis=apis removeAction="removeAction" - wizardFields=wizardFields}} + wizardFields=wizardFields + fieldTypes=fieldTypes}} {{/each}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards-wizard.hbs b/assets/javascripts/discourse/templates/admin-wizards-wizard.hbs index 081cd5f3..c96f8009 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-wizard.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-wizard.hbs @@ -8,7 +8,7 @@ )}} {{d-button - action="createWizard" + action=(route-action "createWizard") label="admin.wizard.create" icon="plus"}}
diff --git a/assets/javascripts/discourse/templates/admin-wizards.hbs b/assets/javascripts/discourse/templates/admin-wizards.hbs index bd575aae..cea77942 100644 --- a/assets/javascripts/discourse/templates/admin-wizards.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards.hbs @@ -2,11 +2,16 @@ {{nav-item route="adminWizardsWizard" label="admin.wizard.nav_label"}} {{nav-item route="adminWizardsCustomFields" label="admin.wizard.custom_field.nav_label"}} {{nav-item route="adminWizardsSubmissions" label="admin.wizard.submissions.nav_label"}} - {{#if siteSettings.wizard_apis_enabled}} + {{#if showApi}} {{nav-item route="adminWizardsApi" label="admin.wizard.api.nav_label"}} {{/if}} {{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}} {{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}} + +
+ {{wizard-subscription-badge}} + {{wizard-subscription-cta}} +
{{/admin-nav}}
diff --git a/assets/javascripts/discourse/templates/components/custom-field-input.hbs b/assets/javascripts/discourse/templates/components/custom-field-input.hbs index 43a97be8..c0bdaaff 100644 --- a/assets/javascripts/discourse/templates/components/custom-field-input.hbs +++ b/assets/javascripts/discourse/templates/components/custom-field-input.hbs @@ -1,17 +1,23 @@ {{#if showInputs}} - {{combo-box + {{wizard-subscription-selector value=field.klass - content=klassContent - none="admin.wizard.custom_field.klass.select" - onChange=(action (mut field.klass))}} + feature="custom_field" + attribute="klass" + onChange=(action (mut field.klass)) + options=(hash + none="admin.wizard.custom_field.klass.select" + )}} - {{combo-box + {{wizard-subscription-selector value=field.type - content=typeContent - none="admin.wizard.custom_field.type.select" - onChange=(action (mut field.type))}} + feature="custom_field" + attribute="type" + onChange=(action (mut field.type)) + options=(hash + none="admin.wizard.custom_field.type.select" + )}} {{input @@ -22,8 +28,10 @@ {{multi-select value=field.serializers content=serializerContent - none="admin.wizard.custom_field.serializers.select" - onChange=(action (mut field.serializers))}} + onChange=(action (mut field.serializers)) + options=(hash + none="admin.wizard.custom_field.serializers.select" + )}} {{#if loading}} @@ -34,17 +42,17 @@ {{/if}} {{/if}} {{d-button - action="destroy" + action=(action "destroy") icon="trash-alt" class="destroy" disabled=destroyDisabled}} {{d-button icon="save" - action="save" + action=(action "save") disabled=saveDisabled class="save"}} {{d-button - action="close" + action=(action "close") icon="times" disabled=closeDisabled}} @@ -69,7 +77,7 @@ {{else}} - {{d-button action="edit" icon="pencil-alt"}} + {{d-button action=(action "edit") icon="pencil-alt"}} {{/if}} {{/if}} diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/discourse/templates/components/custom-wizard-composer-editor.hbs similarity index 89% rename from assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs rename to assets/javascripts/discourse/templates/components/custom-wizard-composer-editor.hbs index be98db8e..baa6a17a 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/discourse/templates/components/custom-wizard-composer-editor.hbs @@ -1,7 +1,7 @@ {{d-editor tabindex=field.tabindex value=composer.reply - placeholderTranslated=replyPlaceholder + placeholderOverride=replyPlaceholder previewUpdated=(action "previewUpdated") markdownOptions=markdownOptions extraButtons=(action "extraButtons") @@ -11,7 +11,7 @@ validation=validation loading=composer.loading showLink=showLink - wizardComposerEvents=true + wizardComposer=true fieldId=field.id disabled=disableTextarea outletArgs=(hash composer=composer editorType="composer")}} diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs b/assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs similarity index 71% rename from assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs rename to assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs index c4403633..f430fb59 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs +++ b/assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs @@ -1,13 +1,13 @@
- {{combo-box + {{wizard-subscription-selector value=action.type - content=actionTypes + feature="action" + attribute="type" onChange=(action "changeType") options=(hash none="admin.wizard.select_type" - )}} + ) + }}
@@ -158,6 +160,44 @@ )}}
+ + {{#if hasEventField}} +
+
+ +
+ +
+ {{wizard-mapper + inputs=action.add_event + property="add_event" + onUpdate=(action "mappedFieldUpdated") + options=(hash + wizardFieldSelection=true + context="action" + )}} +
+
+ {{/if}} + + {{#if hasLocationField}} +
+
+ +
+ +
+ {{wizard-mapper + inputs=action.add_location + property="add_location" + onUpdate=(action "mappedFieldUpdated") + options=(hash + wizardFieldSelection=true + context="action" + )}} +
+
+ {{/if}} {{/if}} {{#if sendMessage}} @@ -346,7 +386,7 @@
- +
@@ -356,14 +396,14 @@ onChange=(action (mut action.notification_level)) options=(hash isDisabled=action.custom_title_enabled - none="admin.wizard.action.watch_categories.select_a_notification_level" + none="admin.wizard.action.watch_x.select_a_notification_level" )}}
- +
@@ -373,7 +413,76 @@
- + +
+ +
+ {{wizard-mapper + inputs=action.usernames + property="usernames" + onUpdate=(action "mappedFieldUpdated") + options=(hash + context="action" + wizardFieldSelection=true + userFieldSelection="key,value" + userSelection="output" + )}} +
+
+{{/if}} + +{{#if watchTags}} +
+
+ +
+ +
+ {{wizard-mapper + inputs=action.tags + property="tags" + onUpdate=(action "mappedFieldUpdated") + options=(hash + textSelection="key,value" + tagSelection="output" + wizardFieldSelection=true + wizardActionSelection=true + userFieldSelection="key,value" + context="action" + )}} +
+
+ +
+
+ +
+ +
+ {{combo-box + value=action.notification_level + content=availableNotificationLevels + onChange=(action (mut action.notification_level)) + options=(hash + isDisabled=action.custom_title_enabled + none="admin.wizard.action.watch_x.select_a_notification_level" + )}} +
+
+ +
+
+ +
+ +
+ {{input type="checkbox" checked=action.wizard_user}} +
+
+ +
+
+
@@ -714,99 +823,90 @@
{{/if}} -{{#if showAdvanced}} - {{wizard-advanced-toggle showAdvanced=action.showAdvanced}} - - {{#if action.showAdvanced}} -
- - {{#if hasCustomFields}} -
-
- -
- -
- {{wizard-mapper - inputs=action.custom_fields - property="custom_fields" - onUpdate=(action "mappedFieldUpdated") - options=(hash - inputTypes="association" - customFieldSelection="key" - wizardFieldSelection="value" - wizardActionSelection="value" - userFieldSelection="value" - keyPlaceholder="admin.wizard.action.custom_fields.key" - context=customFieldsContext - )}} -
-
- {{/if}} - - {{#if sendMessage}} -
-
- -
- -
- {{wizard-mapper - inputs=action.required - property="required" - onUpdate=(action "mappedFieldUpdated") - options=(hash - textSelection="value" - wizardFieldSelection=true - userFieldSelection=true - groupSelection=true - context="action" - )}} -
-
- {{/if}} - - {{#if showPostAdvanced}} -
-
- -
- -
- {{input type="checkbox" checked=action.skip_redirect}} - - - {{i18n "admin.wizard.action.skip_redirect.description" type="topic"}} - -
-
- -
-
- -
- -
- {{input type="checkbox" checked=action.suppress_notifications}} - - - {{i18n "admin.wizard.action.suppress_notifications.description" type="topic"}} - -
-
- {{/if}} - - {{#if routeTo}} -
-
- -
- -
- {{input value=action.code}} -
-
- {{/if}} +{{#if hasCustomFields}} +
+
+
- {{/if}} + +
+ {{wizard-mapper + inputs=action.custom_fields + property="custom_fields" + onUpdate=(action "mappedFieldUpdated") + options=(hash + inputTypes="association" + customFieldSelection="key" + wizardFieldSelection="value" + wizardActionSelection="value" + userFieldSelection="value" + keyPlaceholder="admin.wizard.action.custom_fields.key" + context=customFieldsContext + )}} +
+
+{{/if}} + +{{#if sendMessage}} +
+
+ +
+ +
+ {{wizard-mapper + inputs=action.required + property="required" + onUpdate=(action "mappedFieldUpdated") + options=(hash + textSelection="value" + wizardFieldSelection=true + userFieldSelection=true + groupSelection=true + context="action" + )}} +
+
+{{/if}} + +{{#if showPostAdvanced}} +
+
+ +
+ +
+ {{input type="checkbox" checked=action.skip_redirect}} + + + {{i18n "admin.wizard.action.skip_redirect.description" type="topic"}} + +
+
+ +
+
+ +
+ +
+ {{input type="checkbox" checked=action.suppress_notifications}} + + + {{i18n "admin.wizard.action.suppress_notifications.description" type="topic"}} + +
+
+{{/if}} + +{{#if routeTo}} +
+
+ +
+ +
+ {{input value=action.code}} +
+
{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index f51b9fbb..6273f9a9 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -1,6 +1,6 @@ {{#if showUndo}} {{d-button - action="undoChanges" + action=(action "undoChanges") icon=undoIcon label=undoKey class="undo-changes"}} @@ -19,7 +19,7 @@
- +
{{i18n "admin.wizard.field.required_label"}} {{input type="checkbox" checked=field.required}} @@ -44,7 +44,7 @@ imageUrl=field.image onUploadDone=(action "imageUploadDone") onUploadDeleted=(action "imageUploadDeleted") - type="wizard-step" + type="wizard-field-image" class="no-repeat contain-image" id=(concat "wizard-field-" field.id "-image-upload")}}
@@ -54,7 +54,7 @@
- +
{{combo-box value=field.type @@ -216,76 +216,70 @@
{{tag-group-chooser + id=(concat field.id "-tag-groups") tagGroups=field.tag_groups + onChange=(action (mut field.tag_groups)) }}
+ +
+
+ +
+ +
+ {{input + type="checkbox" + checked=field.can_create_tag}} +
+
{{/if}} -{{#if showAdvanced}} - {{wizard-advanced-toggle showAdvanced=field.showAdvanced}} +{{#wizard-subscription-container}} +
+
+ +
- {{#if field.showAdvanced}} -
+
+ {{wizard-mapper + inputs=field.condition + options=fieldConditionOptions}} +
+
-
-
- -
+
+
+ +
-
- {{wizard-mapper - inputs=field.condition - options=fieldConditionOptions}} -
+
+ {{wizard-mapper + inputs=field.index + options=fieldIndexOptions}} +
+
+ + {{#if isCategory}} +
+
+
-
-
- -
- -
- {{wizard-mapper - inputs=field.index - options=fieldIndexOptions}} -
+
+ {{combo-box + value=field.property + content=categoryPropertyTypes + onChange=(action (mut field.property)) + options=(hash + none="admin.wizard.selector.placeholder.property" + )}}
- - {{#if isCategory}} -
-
- -
- -
- {{combo-box - value=field.property - content=categoryPropertyTypes - onChange=(action (mut field.property)) - options=(hash - none="admin.wizard.selector.placeholder.property" - )}} -
-
- {{/if}} - -
-
- -
-
- {{input - name="key" - value=field.key - class="medium" - placeholderKey="admin.wizard.translation_placeholder"}} -
-
- - {{#if validations}} - {{wizard-realtime-validations field=field validations=validations}} - {{/if}}
{{/if}} -{{/if}} + + {{#if validations}} + {{wizard-realtime-validations field=field validations=validations}} + {{/if}} +{{/wizard-subscription-container}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs index ad4623c2..40ac09e0 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-step.hbs @@ -18,7 +18,7 @@ imageUrl=step.banner onUploadDone=(action "bannerUploadDone") onUploadDeleted=(action "bannerUploadDeleted") - type="wizard-banner" + type="wizard-step-banner" class="no-repeat contain-image" id=(concat "wizard-step-" step.id "-banner-upload")}}
@@ -34,88 +34,72 @@
-{{wizard-advanced-toggle showAdvanced=step.showAdvanced}} - -{{#if step.showAdvanced}} -
- -
-
- -
- -
- {{wizard-mapper - inputs=step.condition - options=stepConditionOptions}} -
+{{#wizard-subscription-container}} +
+
+
-
-
-
-

{{i18n "admin.wizard.step.force_final.label"}}

- {{input type="checkbox" checked=step.force_final}} - {{i18n "admin.wizard.step.force_final.description"}} -
-
- -
-
- -
-
- {{wizard-mapper - inputs=step.required_data - options=(hash - inputTypes="validation" - inputConnector="and" - wizardFieldSelection="value" - userFieldSelection="value" - keyPlaceholder="admin.wizard.submission_key" - context="step" - )}} - {{#if step.required_data}} -
-
- {{i18n "admin.wizard.step.required_data.not_permitted_message"}} -
- {{input value=step.required_data_message}} -
- {{/if}} -
-
- -
-
- -
-
- {{wizard-mapper - inputs=step.permitted_params - options=(hash - pairConnector="set" - inputTypes="association" - keyPlaceholder="admin.wizard.param_key" - valuePlaceholder="admin.wizard.submission_key" - context="step" - )}} -
-
- -
-
- -
-
- {{input - name="key" - value=step.key - placeholderKey="admin.wizard.translation_placeholder"}} -
+
+ {{wizard-mapper + inputs=step.condition + options=stepConditionOptions}}
-{{/if}} + +
+
+
+

{{i18n "admin.wizard.step.force_final.label"}}

+ {{input type="checkbox" checked=step.force_final}} + {{i18n "admin.wizard.step.force_final.description"}} +
+
+ +
+
+ +
+ +
+ {{wizard-mapper + inputs=step.required_data + options=(hash + inputTypes="validation" + inputConnector="and" + wizardFieldSelection="value" + userFieldSelection="value" + keyPlaceholder="admin.wizard.submission_key" + context="step" + )}} + {{#if step.required_data}} +
+
+ {{i18n "admin.wizard.step.required_data.not_permitted_message"}} +
+ {{input value=step.required_data_message}} +
+ {{/if}} +
+
+ +
+
+ +
+
+ {{wizard-mapper + inputs=step.permitted_params + options=(hash + pairConnector="set" + inputTypes="association" + keyPlaceholder="admin.wizard.param_key" + valuePlaceholder="admin.wizard.submission_key" + context="step" + )}} +
+
+{{/wizard-subscription-container}} {{wizard-links itemType="field" @@ -130,5 +114,6 @@ currentFieldId=currentField.id fieldTypes=fieldTypes removeField="removeField" - wizardFields=wizardFields}} + wizardFields=wizardFields + subscribed=subscribed}} {{/each}} diff --git a/assets/javascripts/discourse/templates/components/wizard-links.hbs b/assets/javascripts/discourse/templates/components/wizard-links.hbs index a7a7662e..368acd35 100644 --- a/assets/javascripts/discourse/templates/components/wizard-links.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-links.hbs @@ -4,17 +4,16 @@ {{#if anyLinks}} {{#each links as |link|}}
- {{d-button action="change" actionParam=link.id translatedLabel=link.label class=link.classes}} + {{d-button action=(action "change") actionParam=link.id translatedLabel=link.label class=link.classes}} {{#unless link.first}} - {{d-button action="back" actionParam=link icon="arrow-left" class="back"}} + {{d-button action=(action "back") actionParam=link icon="arrow-left" class="back"}} {{/unless}} {{#unless link.last}} - {{d-button action="forward" actionParam=link icon="arrow-right" class="forward"}} + {{d-button action=(action "forward") actionParam=link icon="arrow-right" class="forward"}} {{/unless}} - {{d-button action="remove" actionParam=link.id icon="times" class="remove"}} + {{d-button action=(action "remove") actionParam=link.id icon="times" class="remove"}}
{{/each}} {{/if}} - {{d-button action="add" label="admin.wizard.add" icon="plus"}} + {{d-button action=(action "add") label="admin.wizard.add" icon="plus"}}
- diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs index 60f4c0cf..cb94b7ae 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs @@ -66,11 +66,13 @@ {{/if}} {{#if showUser}} - {{user-selector - includeMessageableGroups="true" + {{email-group-user-chooser placeholderKey=placeholderKey - usernames=value + value=value autocomplete="discourse" - onChangeCallback=(action "changeUserValue")}} + onChange=(action "changeUserValue") + options=(hash + includeMessageableGroups="true" + )}} {{/if}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper.hbs index 2de35e0d..c0cc6818 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper.hbs @@ -15,6 +15,6 @@ {{#if canAdd}} - {{d-button action="add" label="admin.wizard.add" icon="plus"}} + {{d-button action=(action "add") label="admin.wizard.add" icon="plus"}} {{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-realtime-validations.hbs b/assets/javascripts/discourse/templates/components/wizard-realtime-validations.hbs index cd1298a9..8269d6ca 100644 --- a/assets/javascripts/discourse/templates/components/wizard-realtime-validations.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-realtime-validations.hbs @@ -1,50 +1,53 @@ -

{{i18n "admin.wizard.field.validations.header"}}

- -
    - {{#each-in field.validations as |type props|}} -
  • - -

    {{i18n (concat "admin.wizard.field.validations." type)}}

    - {{input type="checkbox" checked=props.status}} - {{i18n "admin.wizard.field.validations.enabled"}} -
    -
    -
    -
    - +
    + +
    +
    +
      + {{#each-in field.validations as |type props|}} +
    • + +

      {{i18n (concat "admin.wizard.field.validations." type)}}

      + {{input type="checkbox" checked=props.status}} + {{i18n "admin.wizard.field.validations.enabled"}} +
      +
      +
      +
      + +
      +
      + {{category-selector + categories=(get this (concat "validationBuffer." type ".categories")) + onChange=(action "updateValidationCategories" type props) + class="wizard"}} +
      -
      - {{category-selector - categories=(get this (concat "validationBuffer." type ".categories")) - onChange=(action "updateValidationCategories" type props) - class="wizard"}} +
      +
      + +
      +
      + {{input type="number" class="time-n-value" value=props.time_n_value}} + {{combo-box + value=(readonly props.time_unit) + content=timeUnits + class="time-unit-selector" + onChange=(action (mut props.time_unit))}} +
      +
      +
      +
      + +
      +
      + {{radio-button name=(concat type field.id) value="above" selection=props.position}} + {{i18n "admin.wizard.field.validations.above"}} + {{radio-button name=(concat type field.id) value="below" selection=props.position}} + {{i18n "admin.wizard.field.validations.below"}} +
      -
      -
      - -
      -
      - {{input type="number" class="time-n-value" value=props.time_n_value}} - {{combo-box - value=(readonly props.time_unit) - content=timeUnits - class="time-unit-selector" - onChange=(action (mut props.time_unit))}} -
      -
      -
      -
      - -
      -
      - {{radio-button name=(concat type field.id) value="above" selection=props.position}} - {{i18n "admin.wizard.field.validations.above"}} - {{radio-button name=(concat type field.id) value="below" selection=props.position}} - {{i18n "admin.wizard.field.validations.below"}} -
      -
      -
      -
    • - {{/each-in}} -
    +
  • + {{/each-in}} +
+
diff --git a/assets/javascripts/discourse/templates/components/wizard-subscription-badge.hbs b/assets/javascripts/discourse/templates/components/wizard-subscription-badge.hbs new file mode 100644 index 00000000..b2ce05bc5 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-subscription-badge.hbs @@ -0,0 +1,6 @@ + + + +{{label}} diff --git a/assets/javascripts/discourse/templates/components/wizard-subscription-container.hbs b/assets/javascripts/discourse/templates/components/wizard-subscription-container.hbs new file mode 100644 index 00000000..01d436f5 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-subscription-container.hbs @@ -0,0 +1,12 @@ +
+

{{i18n "admin.wizard.subscription.title"}}

+ + + {{d-icon subscribedIcon}} + {{i18n subscribedLabel}} + +
+ +
+ {{yield}} +
diff --git a/assets/javascripts/discourse/templates/components/wizard-subscription-cta.hbs b/assets/javascripts/discourse/templates/components/wizard-subscription-cta.hbs new file mode 100644 index 00000000..00569756 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-subscription-cta.hbs @@ -0,0 +1 @@ +{{d-icon icon}}{{label}} diff --git a/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-header.hbs b/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-header.hbs new file mode 100644 index 00000000..d91e90f1 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-header.hbs @@ -0,0 +1,15 @@ +
+ + {{component selectKit.options.selectedNameComponent + tabindex=tabindex + item=selectedContent + selectKit=selectKit + shouldDisplayClearableButton=shouldDisplayClearableButton + }} + + {{#if subscriptionRequired}} + {{i18n selectorLabel}} + {{/if}} + + {{d-icon caretIcon class="caret-icon"}} +
diff --git a/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-row.hbs b/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-row.hbs new file mode 100644 index 00000000..de24fd75 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-subscription-selector/wizard-subscription-selector-row.hbs @@ -0,0 +1,15 @@ +{{#if icons}} +
+ + {{#each icons as |icon|}} + {{d-icon icon translatedtitle=(dasherize title)}} + {{/each}} +
+{{/if}} + +
+ {{html-safe label}} + {{#if item.subscriptionRequired}} + {{i18n item.selectorLabel}} + {{/if}} +
diff --git a/assets/javascripts/discourse/templates/components/wizard-table-field.hbs b/assets/javascripts/discourse/templates/components/wizard-table-field.hbs new file mode 100644 index 00000000..af30f422 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-table-field.hbs @@ -0,0 +1,161 @@ +{{#if hasValue}} + {{#if isText}} + {{value.value}} + {{/if}} + + {{#if isLongtext}} +
+

+ {{value.value}} +

+ + {{toggleText}} + +
+ {{/if}} + + {{#if isComposer}} +
+

+ {{value.value}} +

+ + {{toggleText}} + +
+ {{/if}} + + {{#if isComposerPreview}} + {{d-icon "comment-alt"}} + + {{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}} + + {{/if}} + + {{#if isTextOnly}} + {{value.value}} + {{/if}} + + {{#if isDate}} + + {{d-icon "calendar"}}{{value.value}} + + {{/if}} + + {{#if isTime}} + + {{d-icon "clock"}}{{value.value}} + + {{/if}} + + {{#if isDateTime}} + + {{d-icon "calendar"}}{{format-date value.value format="medium"}} + + {{/if}} + + {{#if isNumber}} + {{value.value}} + {{/if}} + + {{#if isCheckbox}} + {{#if checkboxValue}} + + {{d-icon "check"}}{{value.value}} + + {{else}} + + {{d-icon "times"}}{{value.value}} + + {{/if}} + {{/if}} + + {{#if isUrl}} + + {{d-icon "link"}} + + {{value.value}} + + + {{/if}} + + {{#if isUpload}} + + {{file.original_filename}} + + {{/if}} + + {{#if isDropdown}} + + {{d-icon "check-square"}} + {{value.value}} + + {{/if}} + + {{#if isTag}} + {{#each value.value as |tag|}} + {{discourse-tag tag}} + {{/each}} + {{/if}} + + {{#if isCategory}} + + {{i18n "admin.wizard.submissions.category_id"}}: + + + {{value.value}} + + {{/if}} + + {{#if isGroup}} + + {{i18n "admin.wizard.submissions.group_id"}}: + + {{value.value}} + {{/if}} + + {{#if isUserSelector}} + {{#each submittedUsers as |user|}} + {{d-icon "user"}} + + {{user.username}} + + {{/each}} + {{/if}} + + {{#if isUser}} + {{#link-to "user" value.username}} + {{avatar value imageSize="tiny"}} + {{/link-to}} + {{/if}} + + {{#if showUsername}} + + {{username}} + + {{/if}} + + {{#if isSubmittedAt}} + + {{raw-date value}} + + {{/if}} +{{else}} + — +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs b/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs index c657049d..37a3e549 100644 --- a/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-text-editor.hbs @@ -6,13 +6,13 @@
{{#if previewEnabled}} {{d-button - action="togglePreview" + action=(action "togglePreview") translatedLabel=previewLabel}} {{/if}} {{#if fieldsEnabled}} {{d-button - action="togglePopover" + action=(action "togglePopover") translatedLabel=popoverLabel}} {{#if showPopover}} diff --git a/assets/javascripts/discourse/templates/custom-wizard-index.hbs b/assets/javascripts/discourse/templates/custom-wizard-index.hbs new file mode 100644 index 00000000..d208851d --- /dev/null +++ b/assets/javascripts/discourse/templates/custom-wizard-index.hbs @@ -0,0 +1,3 @@ +{{#if noAccess}} + {{custom-wizard-no-access text=(i18n noAccessI18nKey) wizardId=wizardId reason=noAccessReason}} +{{/if}} diff --git a/assets/javascripts/wizard/templates/custom.step.hbs b/assets/javascripts/discourse/templates/custom-wizard-step.hbs similarity index 54% rename from assets/javascripts/wizard/templates/custom.step.hbs rename to assets/javascripts/discourse/templates/custom-wizard-step.hbs index 6456a59c..19357063 100644 --- a/assets/javascripts/wizard/templates/custom.step.hbs +++ b/assets/javascripts/discourse/templates/custom-wizard-step.hbs @@ -5,16 +5,16 @@
{{#if showReset}} - {{wizard-i18n "wizard.reset"}} + {{i18n "wizard.reset"}} {{/if}}
{{/if}} {{#if step.permitted}} - {{wizard-step step=step - wizard=wizard - goNext="goNext" - goBack=(action "goBack") - finished="finished" - showMessage="showMessage"}} + {{custom-wizard-step + step=step + wizard=wizard + goNext=(action "goNext") + goBack=(action "goBack") + showMessage=(action "showMessage")}} {{/if}} diff --git a/assets/javascripts/wizard/templates/custom.hbs b/assets/javascripts/discourse/templates/custom-wizard.hbs similarity index 86% rename from assets/javascripts/wizard/templates/custom.hbs rename to assets/javascripts/discourse/templates/custom-wizard.hbs index 4701fec2..f6d6127e 100644 --- a/assets/javascripts/wizard/templates/custom.hbs +++ b/assets/javascripts/discourse/templates/custom-wizard.hbs @@ -1,7 +1,3 @@ -{{#if showCanvas}} - {{wizard-canvas}} -{{/if}} -
{{outlet}} diff --git a/assets/javascripts/discourse/templates/modal/admin-wizards-columns.hbs b/assets/javascripts/discourse/templates/modal/admin-wizards-columns.hbs new file mode 100644 index 00000000..eb5218b1 --- /dev/null +++ b/assets/javascripts/discourse/templates/modal/admin-wizards-columns.hbs @@ -0,0 +1,32 @@ +{{#d-modal-body title="admin.wizard.edit_columns"}} + {{#if loading}} + {{loading-spinner size="large"}} + {{else}} +
+ {{#each model.columns as |column|}} +
+
+ +
+
+ {{/each}} +
+ {{/if}} +{{/d-modal-body}} + + diff --git a/assets/javascripts/discourse/templates/modal/next-session-scheduled.hbs b/assets/javascripts/discourse/templates/modal/next-session-scheduled.hbs index 1b138360..cbc9d610 100644 --- a/assets/javascripts/discourse/templates/modal/next-session-scheduled.hbs +++ b/assets/javascripts/discourse/templates/modal/next-session-scheduled.hbs @@ -9,7 +9,7 @@