diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index 84e055ca..fcf1a1d0 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -7,7 +7,7 @@ on: - main pull_request: schedule: - - cron: '0 0 * * *' + - cron: '0 */12 * * *' jobs: build: @@ -53,26 +53,27 @@ jobs: repository: discourse/discourse fetch-depth: 1 - - run: echo "REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV - shell: bash + - 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 with: - path: plugins/${{ env.REPOSITORY_NAME }} + path: plugins/${{ steps.repo-name.outputs.value }} fetch-depth: 1 - name: Check spec existence id: check_spec uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ env.REPOSITORY_NAME }}/spec" + files: "plugins/${{ steps.repo-name.outputs.value }}/spec" - name: Check qunit existence id: check_qunit uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ env.REPOSITORY_NAME }}/test/javascripts" + files: "plugins/${{ steps.repo-name.outputs.value }}/test/javascripts" - name: Setup Git run: | @@ -105,7 +106,7 @@ jobs: - name: Lint English locale if: matrix.build_type == 'backend' - run: bundle exec ruby script/i18n_lint.rb "plugins/${{ env.REPOSITORY_NAME }}/locales/{client,server}.en.yml" + run: bundle exec ruby script/i18n_lint.rb "plugins/${{ steps.repo-name.outputs.value }}/locales/{client,server}.en.yml" - name: Get yarn cache directory id: yarn-cache-dir @@ -130,9 +131,9 @@ jobs: - name: Plugin RSpec with Coverage if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exists == 'true' - run: SIMPLECOV=1 bin/rake plugin:spec[${{ env.REPOSITORY_NAME }}] + run: SIMPLECOV=1 bin/rake plugin:spec[${{ steps.repo-name.outputs.value }}] - name: Plugin QUnit if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exists == 'true' - run: bundle exec rake plugin:qunit['${{ env.REPOSITORY_NAME }}','1200000'] + run: bundle exec rake plugin:qunit['${{ steps.repo-name.outputs.value }}','1200000'] timeout-minutes: 30 diff --git a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs index 18fd3fdb..b0dd3de6 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-logs.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-logs.hbs @@ -3,7 +3,7 @@ {{d-button label="refresh" - icon="refresh" + icon="sync" action="refresh" class="refresh"}} diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index 5d18328f..8b30ad94 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -1,43 +1,4 @@ -//= require discourse/app/lib/autocomplete -//= require discourse/app/lib/utilities -//= require discourse/app/lib/offset-calculator -//= require discourse/app/lib/lock-on -//= require discourse/app/lib/text-direction -//= require discourse/app/lib/to-markdown -//= require discourse/app/lib/load-script -//= require discourse/app/lib/url -//= require discourse/app/lib/ajax -//= require discourse/app/lib/ajax-error -//= require discourse/app/lib/page-visible -//= require discourse/app/lib/logout -//= require discourse/app/lib/render-tag -//= require discourse/app/lib/notification-levels -//= require discourse/app/lib/computed -//= require discourse/app/lib/user-search -//= require discourse/app/lib/text -//= require discourse/app/lib/formatter -//= require discourse/app/lib/quote -//= require discourse/app/lib/link-mentions -//= require discourse/app/lib/link-hashtags -//= require discourse/app/lib/category-hashtags -//= require discourse/app/lib/tag-hashtags -//= require discourse/app/lib/uploads -//= require discourse/app/lib/category-tag-search -//= require discourse/app/lib/intercept-click -//= require discourse/app/lib/show-modal -//= require discourse/app/lib/key-value-store -//= require discourse/app/lib/settings -//= require discourse/app/lib/user-presence -//= require discourse/app/lib/hash -//= require discourse/app/lib/bookmark -//= require discourse/app/lib/put-cursor-at-end -//= require discourse/app/lib/safari-hacks -//= require discourse/app/lib/preload-store -//= require discourse/app/lib/topic-fancy-title -//= require discourse/app/lib/cookie -//= require discourse/app/lib/public-js-versions -//= require discourse/app/lib/load-oneboxes -//= require discourse/app/lib/highlight-syntax +//= require_tree_discourse discourse/app/lib //= require discourse/app/mixins/singleton //= require discourse/app/mixins/upload @@ -46,35 +7,7 @@ //= require message-bus -//= require discourse/app/models/login-method -//= require discourse/app/models/permission-type -//= require discourse/app/models/archetype -//= require discourse/app/models/rest -//= require discourse/app/models/site -//= require discourse/app/models/category -//= require discourse/app/models/session -//= require discourse/app/models/post-action-type -//= require discourse/app/models/trust-level -//= require discourse/app/models/store -//= require discourse/app/models/result-set -//= require discourse/app/models/bookmark -//= require discourse/app/models/user -//= require discourse/app/models/user-stream -//= require discourse/app/models/user-action -//= require discourse/app/models/user-action-group -//= require discourse/app/models/user-posts-stream -//= require discourse/app/models/badge -//= require discourse/app/models/badge-grouping -//= require discourse/app/models/user-badge -//= require discourse/app/models/topic -//= require discourse/app/models/action-summary -//= require discourse/app/models/user-action-stat -//= require discourse/app/models/user-drafts-stream -//= require discourse/app/models/user-draft -//= require discourse/app/models/composer -//= require discourse/app/models/draft -//= require discourse/app/models/group -//= require discourse/app/models/group-history +//= require_tree_discourse discourse/app/models //= require discourse/app/helpers/category-link //= require discourse/app/helpers/user-avatar diff --git a/assets/javascripts/wizard/components/wizard-date-input.js.es6 b/assets/javascripts/wizard/components/wizard-date-input.js.es6 index 93c7ed2d..bb11b655 100644 --- a/assets/javascripts/wizard/components/wizard-date-input.js.es6 +++ b/assets/javascripts/wizard/components/wizard-date-input.js.es6 @@ -1,3 +1,42 @@ import DateInput from "discourse/components/date-input"; +import loadScript from "discourse/lib/load-script"; +import discourseComputed from "discourse-common/utils/decorators"; +import I18n from "I18n"; +/* global Pikaday:true */ -export default DateInput.extend(); +export default DateInput.extend({ + useNativePicker: false, + + @discourseComputed() + placeholder() { + return this.format; + }, + + _loadPikadayPicker(container) { + return loadScript("/javascripts/pikaday.js").then(() => { + let defaultOptions = { + field: this.element.querySelector(".date-picker"), + container: container || this.element.querySelector(".picker-container"), + bound: container === null, + format: this.format, + firstDay: 1, + i18n: { + previousMonth: I18n.t("dates.previous_month"), + nextMonth: I18n.t("dates.next_month"), + months: moment.months(), + weekdays: moment.weekdays(), + weekdaysShort: moment.weekdaysShort(), + }, + onSelect: (date) => this._handleSelection(date), + }; + + if (this.relativeDate) { + defaultOptions = Object.assign({}, defaultOptions, { + minDate: moment(this.relativeDate).toDate(), + }); + } + + return new Pikaday(Object.assign({}, defaultOptions, this._opts())); + }); + }, +}); diff --git a/assets/javascripts/wizard/lib/wizard-i18n.js.es6 b/assets/javascripts/wizard/lib/wizard-i18n.js.es6 index 17242e58..fdefab77 100644 --- a/assets/javascripts/wizard/lib/wizard-i18n.js.es6 +++ b/assets/javascripts/wizard/lib/wizard-i18n.js.es6 @@ -1,7 +1,10 @@ import I18n from "I18n"; const getThemeId = () => { - let themeId = parseInt($("meta[name=discourse_theme_ids]")[0].content, 10); + let themeId = parseInt( + document.querySelector("meta[name=discourse_theme_id]").content, + 10 + ); if (!isNaN(themeId)) { return themeId.toString(); diff --git a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs index 4ac6571b..ed4d14e3 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-date.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-date.hbs @@ -2,4 +2,5 @@ date=date onChange=(action "onChange") tabindex=field.tabindex + format=field.format }} diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index fd93ef15..e0cf669d 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -6,7 +6,7 @@ class CustomWizard::WizardController < ::ApplicationController before_action :ensure_plugin_enabled helper_method :wizard_page_title - helper_method :wizard_theme_ids + helper_method :wizard_theme_id helper_method :wizard_theme_lookup helper_method :wizard_theme_translations_lookup @@ -20,16 +20,16 @@ class CustomWizard::WizardController < ::ApplicationController wizard ? (wizard.name || wizard.id) : I18n.t('wizard.custom_title') end - def wizard_theme_ids - wizard ? [wizard.theme_id] : nil + def wizard_theme_id + wizard ? wizard.theme_id : nil end def wizard_theme_lookup(name) - Theme.lookup_field(wizard_theme_ids, mobile_view? ? :mobile : :desktop, name) + Theme.lookup_field(wizard_theme_id, mobile_view? ? :mobile : :desktop, name) end def wizard_theme_translations_lookup - Theme.lookup_field(wizard_theme_ids, :translations, I18n.locale) + Theme.lookup_field(wizard_theme_id, :translations, I18n.locale) end def index diff --git a/lib/custom_wizard/exceptions/exceptions.rb b/lib/custom_wizard/exceptions/exceptions.rb new file mode 100644 index 00000000..b5014d27 --- /dev/null +++ b/lib/custom_wizard/exceptions/exceptions.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +module CustomWizard + class SprocketsFileNotFound < StandardError; end + class SprocketsEmptyPath < StandardError; end +end diff --git a/lib/custom_wizard/validators/update.rb b/lib/custom_wizard/validators/update.rb index d84b448a..93d4955f 100644 --- a/lib/custom_wizard/validators/update.rb +++ b/lib/custom_wizard/validators/update.rb @@ -52,7 +52,7 @@ class ::CustomWizard::UpdateValidator @updater.errors.add(field_id, I18n.t('wizard.field.invalid_file', label: label, types: file_types)) end - if ['date', 'date_time'].include?(type) && value.present? && !validate_date(value) + if ['date', 'date_time'].include?(type) && value.present? && !validate_date(value, format) @updater.errors.add(field_id, I18n.t('wizard.field.invalid_date')) end @@ -88,13 +88,8 @@ class ::CustomWizard::UpdateValidator .include?(File.extname(value['original_filename'])[1..-1]) end - def validate_date(value) - begin - Date.parse(value) - true - rescue ArgumentError - false - end + def validate_date(value, format) + v8.eval("moment('#{value}', '#{format}', true).isValid()") end def validate_time(value) @@ -126,4 +121,12 @@ class ::CustomWizard::UpdateValidator def standardise_boolean(value) ActiveRecord::Type::Boolean.new.cast(value) end + + def v8 + return @ctx if @ctx + + @ctx = PrettyText.v8 + PrettyText.ctx_load(@ctx, "#{Rails.root}/vendor/assets/javascripts/moment.js") + @ctx + end end diff --git a/plugin.rb b/plugin.rb index b335971c..f6342f3f 100644 --- a/plugin.rb +++ b/plugin.rb @@ -35,6 +35,22 @@ if respond_to?(:register_svg_icon) register_svg_icon "save" end +class ::Sprockets::DirectiveProcessor + def process_require_tree_discourse_directive(path = ".") + raise CustomWizard::SprocketsEmptyPath, "path cannot be empty" if path == "." + + discourse_asset_path = "#{Rails.root}/app/assets/javascripts/" + path = File.expand_path(path, discourse_asset_path) + stat = @environment.stat(path) + + if stat && stat.directory? + require_paths(*@environment.stat_sorted_tree_with_dependencies(path)) + else + raise CustomWizard::SprocketsFileNotFound, "#{path} not found in discourse core" + end + end +end + after_initialize do %w[ ../lib/custom_wizard/engine.rb @@ -74,6 +90,7 @@ after_initialize do ../lib/custom_wizard/api/endpoint.rb ../lib/custom_wizard/api/log_entry.rb ../lib/custom_wizard/liquid_extensions/first_non_empty.rb + ../lib/custom_wizard/exceptions/exceptions.rb ../serializers/custom_wizard/api/authorization_serializer.rb ../serializers/custom_wizard/api/basic_endpoint_serializer.rb ../serializers/custom_wizard/api/endpoint_serializer.rb diff --git a/serializers/custom_wizard/wizard_serializer.rb b/serializers/custom_wizard/wizard_serializer.rb index f858c195..7a162ba5 100644 --- a/serializers/custom_wizard/wizard_serializer.rb +++ b/serializers/custom_wizard/wizard_serializer.rb @@ -8,11 +8,11 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer :completed, :required, :permitted, - :uncategorized_category_id + :uncategorized_category_id, + :categories has_many :steps, serializer: ::CustomWizard::StepSerializer, embed: :objects has_one :user, serializer: ::BasicUserSerializer, embed: :objects - has_many :categories, serializer: ::BasicCategorySerializer, embed: :objects has_many :groups, serializer: ::BasicGroupSerializer, embed: :objects def completed @@ -56,4 +56,8 @@ class CustomWizard::WizardSerializer < CustomWizard::BasicWizardSerializer def include_uncategorized_category_id? object.needs_categories end + + def categories + object.categories.map { |c| c.to_h } + end end diff --git a/spec/components/custom_wizard/update_validator_spec.rb b/spec/components/custom_wizard/update_validator_spec.rb index 81212b4b..e7658d8c 100644 --- a/spec/components/custom_wizard/update_validator_spec.rb +++ b/spec/components/custom_wizard/update_validator_spec.rb @@ -132,4 +132,44 @@ describe CustomWizard::UpdateValidator do updater.errors.messages[:step_2_field_6].first ).to eq(nil) end + + it 'validates date fields' do + @template[:steps][1][:fields][0][:format] = "DD-MM-YYYY" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_1: '13-11-2021') + expect( + updater.errors.messages[:step_2_field_1].first + ).to eq(nil) + end + + it 'doesn\'t validate date field if the format is not respected' do + @template[:steps][1][:fields][0][:format] = "MM-DD-YYYY" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_1: '13-11-2021') + expect( + updater.errors.messages[:step_2_field_1].first + ).to eq(I18n.t('wizard.field.invalid_date')) + end + + it 'validates date time fields' do + @template[:steps][1][:fields][2][:format] = "DD-MM-YYYY HH:mm:ss" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_3: '13-11-2021 09:15:00') + expect( + updater.errors.messages[:step_2_field_3].first + ).to eq(nil) + end + + it 'doesn\'t validate date time field if the format is not respected' do + @template[:steps][1][:fields][2][:format] = "MM-DD-YYYY HH:mm:ss" + CustomWizard::Template.save(@template) + + updater = perform_validation('step_2', step_2_field_3: '13-11-2021 09:15') + expect( + updater.errors.messages[:step_2_field_3].first + ).to eq(I18n.t('wizard.field.invalid_date')) + end end diff --git a/spec/extensions/sprockets_directive_spec.rb b/spec/extensions/sprockets_directive_spec.rb new file mode 100644 index 00000000..5a074040 --- /dev/null +++ b/spec/extensions/sprockets_directive_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative '../plugin_helper' + +describe "Sprockets: require_tree_discourse directive" do + let(:discourse_asset_path) { + "#{Rails.root}/app/assets/javascripts/" + } + let(:fixture_asset_path) { + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/" + } + let(:test_file_contents) { + "console.log('hello')" + } + let(:resolved_file_contents) { + File.read( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/resolved_js_file_contents.txt" + ) + } + + before do + @env ||= Sprockets::Environment.new + discourse_asset_path = "#{Rails.root}/app/assets/javascripts/" + fixture_asset_path = "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/sprockets/" + @env.append_path(discourse_asset_path) + @env.append_path(fixture_asset_path) + @env.cache = {} + end + + def create_tmp_folder_and_run(path, file_contents, &block) + dir = File.dirname(path) + unless File.directory?(dir) + FileUtils.mkdir_p(dir) + end + + File.new(path, 'w') + File.write(path, file_contents) + yield block if block_given? + FileUtils.rm_r(dir) + end + + it "includes assets from the discourse core" do + create_tmp_folder_and_run("#{discourse_asset_path}/sptest/test.js", test_file_contents) do + expect(@env.find_asset("require_tree_discourse_test.js").to_s).to eq(resolved_file_contents) + end + end + + it "throws ArgumentError if path is empty" do + expect { @env.find_asset("require_tree_discourse_empty.js") }.to raise_error(CustomWizard::SprocketsEmptyPath).with_message("path cannot be empty") + end + + it "throws ArgumentError if path is non non-existent" do + expect { @env.find_asset("require_tree_discourse_non_existant.js") }.to raise_error(CustomWizard::SprocketsFileNotFound) + end +end diff --git a/spec/fixtures/sprockets/require_tree_discourse_empty.js b/spec/fixtures/sprockets/require_tree_discourse_empty.js new file mode 100644 index 00000000..df264ec5 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_empty.js @@ -0,0 +1 @@ +//= require_tree_discourse \ No newline at end of file diff --git a/spec/fixtures/sprockets/require_tree_discourse_non_existant.js b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js new file mode 100644 index 00000000..d9b2be76 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js @@ -0,0 +1 @@ +//= require_tree_discourse dummy_path \ No newline at end of file diff --git a/spec/fixtures/sprockets/require_tree_discourse_test.js b/spec/fixtures/sprockets/require_tree_discourse_test.js new file mode 100644 index 00000000..a86aa0d7 --- /dev/null +++ b/spec/fixtures/sprockets/require_tree_discourse_test.js @@ -0,0 +1 @@ +//= require_tree_discourse sptest \ No newline at end of file diff --git a/spec/fixtures/sprockets/resolved_js_file_contents.txt b/spec/fixtures/sprockets/resolved_js_file_contents.txt new file mode 100644 index 00000000..53e2cfa2 --- /dev/null +++ b/spec/fixtures/sprockets/resolved_js_file_contents.txt @@ -0,0 +1,3 @@ +eval("define(\"sptest/test\", [], function () {\n \"use strict\";\n\n console.log('hello');\n});" + "\n//# sourceURL=sptest/test"); +; +eval("" + "\n//# sourceURL=require_tree_discourse_test"); diff --git a/views/layouts/wizard.html.erb b/views/layouts/wizard.html.erb index efa09734..16d119b7 100644 --- a/views/layouts/wizard.html.erb +++ b/views/layouts/wizard.html.erb @@ -4,8 +4,8 @@ <%= discourse_stylesheet_link_tag :wizard, theme_id: nil %> <%= discourse_stylesheet_link_tag :wizard_custom %> - <%- if wizard_theme_ids.present? %> - <%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme), theme_ids: wizard_theme_ids %> + <%- if wizard_theme_id.present? %> + <%= discourse_stylesheet_link_tag (mobile_view? ? :mobile_theme : :desktop_theme), theme_id: wizard_theme_id %> <%- end %> <%= preload_script "locales/#{I18n.locale}" %> @@ -29,7 +29,7 @@ <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %> - "> + <%= render partial: "layouts/head" %>