diff --git a/Gemfile.lock b/Gemfile.lock index b2aa04fc..9fe60a56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,8 @@ GEM coderay (1.1.2) concurrent-ruby (1.0.5) crass (1.0.4) + cypress-on-rails (1.1.1) + rack database_cleaner (1.7.0) devise (4.4.3) bcrypt (~> 3.0) @@ -338,6 +340,7 @@ DEPENDENCIES binda! bullet (>= 5.6, < 6) capybara (>= 2.14, < 3) + cypress-on-rails (~> 1.1) database_cleaner (>= 1.6, < 2) factory_bot_rails (~> 4.8) generator_spec (~> 0.9.4) @@ -356,4 +359,4 @@ DEPENDENCIES yard-activesupport-concern (~> 0.0.1, < 0.1) BUNDLED WITH - 1.16.2 + 1.16.6 diff --git a/app/assets/stylesheets/binda/components/form_item.scss b/app/assets/stylesheets/binda/components/form_item.scss index 9fa46fad..67c012da 100644 --- a/app/assets/stylesheets/binda/components/form_item.scss +++ b/app/assets/stylesheets/binda/components/form_item.scss @@ -8,6 +8,15 @@ padding: 12px; } +.form-item--sixth-size { + display: inline-block; + width: calc(16.6% - 3px); + + .form-group { + border-top: none !important; + } +} + .form-item--half-size { // OLD SOLUTION (nicer, but can't be used with max-height=0 and overflow-y=hidden) // position: relative; diff --git a/app/controllers/binda/boards_controller.rb b/app/controllers/binda/boards_controller.rb index aa77a522..fefe4393 100644 --- a/app/controllers/binda/boards_controller.rb +++ b/app/controllers/binda/boards_controller.rb @@ -18,6 +18,7 @@ def update if @board.update(board_params) redirect_to structure_board_path(@structure.slug, @board.slug), notice: 'Setting was successfully updated.' else + @fields_errors = get_errors(@board) render :edit, flash: { alert: @board.errors } end end @@ -86,5 +87,6 @@ def sort_repeaters_by(repeaters) Repeater.find(id).update_column('position', i+1) end end + end end diff --git a/app/controllers/binda/components_controller.rb b/app/controllers/binda/components_controller.rb index 1c3f9cb2..81d62728 100644 --- a/app/controllers/binda/components_controller.rb +++ b/app/controllers/binda/components_controller.rb @@ -46,6 +46,7 @@ def update if @component.update(component_params) redirect_to structure_component_path(@structure.slug, @component.slug), notice: "#{ @structure.name.capitalize } was successfully updated." else + @fields_errors = get_errors(@component) render :edit, flash: { alert: @component.errors } end end diff --git a/app/controllers/binda/field_groups_controller.rb b/app/controllers/binda/field_groups_controller.rb index 556771c8..265ec450 100644 --- a/app/controllers/binda/field_groups_controller.rb +++ b/app/controllers/binda/field_groups_controller.rb @@ -95,7 +95,7 @@ def set_field_group def field_group_params params.require(:field_group).permit( :name, :slug, :description, :position, :layout, :structure_id, field_settings_attributes: [ - :id, :field_group_id, :field_setting_id, :name, :slug, :description, :field_type, :position, :required, :read_only, :default_text, :ancestry, :default_choice_id, :allow_null, accepted_structure_ids: [], choices: [], choices_attributes: [ + :id, :field_group_id, :field_setting_id, :name, :slug, :description, :field_type, :position, :required, :read_only, :has_preview, :default_text, :ancestry, :default_choice_id, :allow_null, accepted_structure_ids: [], choices: [], choices_attributes: [ :id, :field_setting_id, :label, :value ] ] @@ -105,7 +105,7 @@ def field_group_params def new_params params.require(:field_group).permit( new_field_settings: [ - :id, :field_group_id, :field_setting_id, :name, :slug, :description, :field_type, :position, :required, :read_only, :ancestry, :default_choice_id, :allow_null, choices: [] + :id, :field_group_id, :field_setting_id, :name, :slug, :description, :field_type, :position, :required, :read_only, :has_preview, :ancestry, :default_choice_id, :allow_null, choices: [] ], new_choices: [ :id, :field_setting_id, :label, :value diff --git a/app/controllers/binda/field_settings_controller.rb b/app/controllers/binda/field_settings_controller.rb index 4a2cbff7..20885c46 100644 --- a/app/controllers/binda/field_settings_controller.rb +++ b/app/controllers/binda/field_settings_controller.rb @@ -72,7 +72,7 @@ def set_field_setting # Only allow a trusted parameter "white list" through. def field_setting_params - params.require(:field_setting).permit(:name, :slug, :description, :position, :required, :default_text, :field_group_id, :field_type ) + params.require(:field_setting).permit(:name, :slug, :description, :position, :preview, :required, :default_text, :field_group_id, :field_type ) end end end diff --git a/app/controllers/concerns/binda/fieldable_helpers.rb b/app/controllers/concerns/binda/fieldable_helpers.rb index 324fefb0..cec9f0e9 100644 --- a/app/controllers/concerns/binda/fieldable_helpers.rb +++ b/app/controllers/concerns/binda/fieldable_helpers.rb @@ -1,6 +1,6 @@ module Binda module FieldableHelpers - + extend ActiveSupport::Concern # Only allow a trusted parameter "white list" through. @@ -173,5 +173,28 @@ def return_audio_details asset def bytes_to_megabytes bytes (bytes.to_f / 1.megabyte).round(2) end + + # When a field belonging to a component has a error, this method + # will store the class name and id. Useful to check the status of + # a field on the front end application. + # + # @return {array} Array of objects containing class name and id of the fields with errors + def get_errors(instance) + fields_errors = {} + instance.errors.details.keys.each do |key| + instance.errors.details[key].each do |obj| + obj[:value].each do |obj2| + newKey = "#{obj2.class.name.parameterize}--#{obj2.id.to_s}" # eg: binda-image--33 + fields_errors[newKey] = { + class: obj2.class.name, + id: obj2.id, + message: obj2.errors.full_messages.join('. ') + } + end + end + end + fields_errors + end + end end \ No newline at end of file diff --git a/app/helpers/binda/application_helper.rb b/app/helpers/binda/application_helper.rb index 6a9734e3..ee815e13 100644 --- a/app/helpers/binda/application_helper.rb +++ b/app/helpers/binda/application_helper.rb @@ -19,7 +19,14 @@ def is_devise_controller def get_form_manage_user_url return manage_users_path if action_name == 'new' return manage_user_path if action_name == 'edit' - end + end + + def check_field_errors(instance, attribute) + key = "#{instance.class.name.parameterize}--#{instance.id.to_s}" + unless @fields_errors[key].nil? + instance.errors.add(attribute, @fields_errors[key][:message]); + end + end end end diff --git a/app/models/binda/asset.rb b/app/models/binda/asset.rb index d9f93d9a..45fc07c5 100644 --- a/app/models/binda/asset.rb +++ b/app/models/binda/asset.rb @@ -4,6 +4,6 @@ module Binda class Asset < ApplicationRecord include Fields - + include FieldReadonly end end \ No newline at end of file diff --git a/app/models/binda/audio.rb b/app/models/binda/audio.rb index 62c728ad..7888bfa3 100644 --- a/app/models/binda/audio.rb +++ b/app/models/binda/audio.rb @@ -3,6 +3,5 @@ module Binda class Audio < Asset mount_uploader :audio, AudioUploader - - end + end end \ No newline at end of file diff --git a/app/models/binda/image.rb b/app/models/binda/image.rb index 4d068253..e763b1fb 100644 --- a/app/models/binda/image.rb +++ b/app/models/binda/image.rb @@ -18,7 +18,7 @@ def register_details register_details_of(file) end end - + # Register image details # # This method is used by register_details in a rake task diff --git a/app/models/binda/selection.rb b/app/models/binda/selection.rb index 09d24765..00e9e64e 100644 --- a/app/models/binda/selection.rb +++ b/app/models/binda/selection.rb @@ -134,6 +134,7 @@ class Selection < ApplicationRecord include Fields include FieldUniqueness + include FieldReadonly has_and_belongs_to_many :choices validate :has_choices diff --git a/app/models/binda/text.rb b/app/models/binda/text.rb index dfe8b56d..efa536f3 100644 --- a/app/models/binda/text.rb +++ b/app/models/binda/text.rb @@ -6,9 +6,10 @@ module Binda # this field is represented by a WYSIWYG. But this is just a admin panel convention: the class can infact store # a simple string of text as well. class Text < ApplicationRecord - + include Fields include FieldUniqueness - + include FieldReadonly + end end diff --git a/app/models/concerns/binda/field_readonly.rb b/app/models/concerns/binda/field_readonly.rb new file mode 100644 index 00000000..cdabb981 --- /dev/null +++ b/app/models/concerns/binda/field_readonly.rb @@ -0,0 +1,31 @@ +module Binda + module FieldReadonly + + extend ActiveSupport::Concern + + included do + validate :is_read_only + end + + def is_read_only + field_setting = self.field_setting + if field_setting.read_only? + # check if content is changed + # load previous db instance + db_instance = "Binda::#{field_setting.field_type.classify}".constantize.find_by( + fieldable_id: self.fieldable_id, fieldable_type: self.fieldable_type, field_setting_id: field_setting.id ) + + # check if active record instance you want to save is different + # generate an error if it is + # else do nothing + if self.persisted? && db_instance.attributes != self.attributes + errors.add(:base, I18n.t("binda.readonly_validation", { + arg1: self.class.name, + arg2: self.field_setting.name, + arg3: self.id + })) + end + end + end + end +end \ No newline at end of file diff --git a/app/models/concerns/binda/fieldable_associations.rb b/app/models/concerns/binda/fieldable_associations.rb index d6ea6aec..20fc99ca 100644 --- a/app/models/concerns/binda/fieldable_associations.rb +++ b/app/models/concerns/binda/fieldable_associations.rb @@ -127,9 +127,9 @@ def generate_fields end # If this is a repeater else - self.field_setting.children.each do |field_setting| + self.field_setting.children.each do |field_setting| "Binda::#{field_setting.field_type.classify}".constantize.find_or_create_by!( - fieldable_id: self.id, fieldable_type: self.class.name, field_setting_id: field_setting.id ) + fieldable_id: self.id, fieldable_type: self.class.name, field_setting_id: field_setting.id ) end end end diff --git a/app/models/concerns/binda/fields.rb b/app/models/concerns/binda/fields.rb index 3720ac3e..0c512168 100644 --- a/app/models/concerns/binda/fields.rb +++ b/app/models/concerns/binda/fields.rb @@ -14,8 +14,9 @@ module Fields validates :field_setting, presence: true validates :fieldable_id, presence: true - validates :fieldable_type, presence: true + validates :fieldable_type, presence: true + include ActiveModel::Validations end - end end + diff --git a/app/views/binda/components/edit.html.erb b/app/views/binda/components/edit.html.erb index 8ef372f6..1a51c7ad 100644 --- a/app/views/binda/components/edit.html.erb +++ b/app/views/binda/components/edit.html.erb @@ -3,7 +3,6 @@ <%= @structure.name.humanize.split.map(&:capitalize).join(' ') %> <% end %> - <% content_for :header do %>
<% if @structure.has_preview %> diff --git a/app/views/binda/components/index.html.erb b/app/views/binda/components/index.html.erb index be647770..e336fcb6 100644 --- a/app/views/binda/components/index.html.erb +++ b/app/views/binda/components/index.html.erb @@ -2,7 +2,6 @@ <%= @structure.name.humanize.split.map(&:capitalize).join(' ').pluralize %> <% end %> - <% content_for :header do %>
<% end %> - <% content_for :content do %>
-
+
<%= ff.input :read_only, as: :boolean, label: " #{ t('binda.read_only')}".html_safe, wrapper_html: { class: 'standard-form--radio' } %>
+
+ <%= ff.input :has_preview, as: :boolean, label: " #{ t('binda.preview')}".html_safe, wrapper_html: { class: 'standard-form--radio' } %> +
+
+ <%= ff.input :required, as: :boolean, label: " #{ t('binda.required')}".html_safe, wrapper_html: { class: 'standard-form--radio' } %> +
<%= ff.input :description, input_html: { class: "form-item--input tinymce" } %>
diff --git a/app/views/binda/fieldable/_form_item_image.html.erb b/app/views/binda/fieldable/_form_item_image.html.erb index cbfd4d1c..3180f645 100644 --- a/app/views/binda/fieldable/_form_item_image.html.erb +++ b/app/views/binda/fieldable/_form_item_image.html.erb @@ -8,21 +8,41 @@

<% image = ff.object.image.thumb.url if ff.object.image.present? %>
- <%= ff.input :image, - label: " #{t('binda.choose_file_button')}".html_safe, - hint: field_setting.description.nil? ? false : field_setting.description.html_safe, - url: url_for([ff.object, action: :remove_image]).html_safe, - disabled: field_setting.read_only?, - object: ff.object, - wrapper: false, - input_html: { - 'data-url': url_for([ - @instance.structure, - @instance, - action: :upload, - repeater: { id: "#{field_setting.parent_id}" }]), - 'data-id': ff.object.id - } %> + <% # TODO test! %> + <% check_field_errors(ff.object, :image) unless @fields_errors.nil? %> + <% if ff.object.field_setting.read_only? %> + <%= ff.input :image, + label: " #{t('binda.choose_file_button')}".html_safe, + hint: field_setting.description.nil? ? false : field_setting.description.html_safe, + url: url_for([ff.object, action: :remove_image]).html_safe, + disabled: field_setting.read_only?, + object: ff.object, + wrapper: :file_read_only, + input_html: { + 'data-url': url_for([ + @instance.structure, + @instance, + action: :upload, + repeater: { id: "#{field_setting.parent_id}" }]), + 'data-id': ff.object.id + } %> + <% else %> + <%= ff.input :image, + label: " #{t('binda.choose_file_button')}".html_safe, + hint: field_setting.description.nil? ? false : field_setting.description.html_safe, + url: url_for([ff.object, action: :remove_image]).html_safe, + disabled: field_setting.read_only?, + object: ff.object, + wrapper: :file_uploadable, + input_html: { + 'data-url': url_for([ + @instance.structure, + @instance, + action: :upload, + repeater: { id: "#{field_setting.parent_id}" }]), + 'data-id': ff.object.id + } %> + <% end %>
<%= ff.input :image_cache, as: :hidden %> <%= ff.input :field_setting_id, as: :hidden, input_html: { value: field_setting.id } %> diff --git a/app/views/binda/fieldable/_form_item_string.html.erb b/app/views/binda/fieldable/_form_item_string.html.erb index b79bcca9..cfccda6f 100644 --- a/app/views/binda/fieldable/_form_item_string.html.erb +++ b/app/views/binda/fieldable/_form_item_string.html.erb @@ -1,7 +1,7 @@
- <%= ff.input :content, - input_html: { value: ff.object.content }, + <%= ff.input :content, + input_html: { value: ff.object.content }, as: :string, disabled: field_setting.read_only?, hint: field_setting.description.nil? ? false : field_setting.description.html_safe, diff --git a/app/views/binda/fieldable/_form_item_text.html.erb b/app/views/binda/fieldable/_form_item_text.html.erb index ff800c84..593e8a77 100644 --- a/app/views/binda/fieldable/_form_item_text.html.erb +++ b/app/views/binda/fieldable/_form_item_text.html.erb @@ -1,7 +1,8 @@
- - <%= ff.input :content, - input_html: { value: ff.object.content, class: ( field_setting.read_only? ? '' : 'tinymce' ) }, + <% # TODO test! %> + <% check_field_errors(ff.object, :content) unless @fields_errors.nil? %> + <%= ff.input :content, + input_html: { value: ff.object.content, class: ( field_setting.read_only? ? '' : 'tinymce' ) }, as: :text, disabled: field_setting.read_only?, hint: field_setting.description.nil? ? false : field_setting.description.html_safe, @@ -12,4 +13,4 @@ <%= ff.input :fieldable_id, as: :hidden %> <%= ff.input :fieldable_type, as: :hidden %> -
+
\ No newline at end of file diff --git a/app/views/layouts/binda/_form_errors.html.erb b/app/views/layouts/binda/_form_errors.html.erb index f29983a7..3ca1c3fa 100644 --- a/app/views/layouts/binda/_form_errors.html.erb +++ b/app/views/layouts/binda/_form_errors.html.erb @@ -5,6 +5,16 @@ <% f.object.errors.full_messages.each do |msg| %>
  • <%= msg %>
  • <% end %> + <% f.object.errors.details.keys.each do |key| + f.object.errors.details[key].each do |obj| + obj[:value].each do |obj2| %> + <% obj2.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end + end + end + end + %>
    <% end %> \ No newline at end of file diff --git a/binda.gemspec b/binda.gemspec index 449c37ed..dd298b79 100644 --- a/binda.gemspec +++ b/binda.gemspec @@ -59,5 +59,6 @@ Gem::Specification.new do |s| s.add_development_dependency "rubocop", "~> 0.52.1" s.add_development_dependency "mry", "~> 0.52" s.add_development_dependency "generator_spec", "~> 0.9.4" + s.add_development_dependency "cypress-on-rails", "~> 1.1" end diff --git a/config/initializers/cypress_dev.rb b/config/initializers/cypress_dev.rb new file mode 100644 index 00000000..a6523395 --- /dev/null +++ b/config/initializers/cypress_dev.rb @@ -0,0 +1,9 @@ +if defined?(CypressDev) + CypressDev.configure do |c| + c.cypress_folder = File.expand_path("#{__dir__}/../../spec/cypress") + # WARNING!! CypressDev can execute arbitrary ruby code + # please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0 + c.use_middleware = Rails.env.test? + c.logger = Rails.logger + end +end \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 2abb684b..5d7f5baa 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -16,8 +16,7 @@ # by default. You can change it below and use your own secret key. # config.secret_key = 'a8395fa034e067da620d70f7f39ad4765d163e44ce05d5c243393e1bbfdc10f9a464aaf34fcac3291a6746424ea2b9f9564a01c31820e28bf1737c16d139cd5c' # binda.hook.1 - config.secret_key = '81113efa8b7abba0674471b5b4429cb9da5182d3eab248be196f58f62f8de2c3a7174fcf0f17fbf32bb0e76ba46c02410fecef1101d9da64c010bab2104aba6e' - config.secret_key = '940fcbf616f851acdbcfb5af5629435e0ce550b51a0bf1db4cfb609b537d93b98a9ddefbf03a8274fa59a6483fe82df3aa1a6609a8ca0e62ae3269a91374cccd' + config.secret_key = 'b92bf46fe2bb9554025eaaa638e127a97958df22e965fbc4cc065e0b47abdcd63029442b5714b2824784f3149a585cf5c7bf5a6c73cbb2f5a167b75180ec0fe2' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -115,8 +114,7 @@ # Set up a pepper to generate the hashed password. # config.pepper = '124ef1b08a92f891a392a9594b868887c945ca8438bd7852f8ac4d91e51f2cc699b8b6c7b12057c0f0e6dc03d04f8ee4eab72278b0f948e2c1e43f8b449c2262' # binda.hook.2 - config.pepper = '03af0d695bb86b8274e1f93261a7f885ca85f0e2356cd3fd279ad13ac70af811a1f7d221de07c137aadc327e1f95b3b00ca6ed8836d09010893631c1eb01a36c' - config.pepper = '0586297a053d9c535c607464fb45c15325e4faa6f31f1b5361a431c1978c2dce2776047cef7b89500600bbfa82818b853ab896ad8b48334656bb907a06c8a9bb' + config.pepper = 'fb7dd6ba2c94638fdf2c4500f44788e7d491b9cb985be333f6c2c83514ce6759cab1e05ba9e02cb568231239f8054878984d1670bb269162f0139c402340de12' # Send a notification email when the user's password is changed # config.send_password_change_notification = false diff --git a/config/initializers/simple_form__fileupload.rb b/config/initializers/simple_form__fileupload.rb index c3d1d237..7dce10d9 100644 --- a/config/initializers/simple_form__fileupload.rb +++ b/config/initializers/simple_form__fileupload.rb @@ -8,38 +8,40 @@ module Previews def preview(wrapper_options = nil) @preview ||= begin # open a div tag for the file preview - html = '

    #{ t('binda.no_preview')}

    " - # Add no-preview text - html << "\">

    #{ t('binda.no_preview')}

    " - - # Add video tag - if options[:object].video.present? - html << "" - html << "" - elsif options[:object].audio.present? - html << "" - html << "" - else - html << "" - html << "" - end + # Add video tag + if options[:object].video.present? + html << "" + html << "" + elsif options[:object].audio.present? + html << "" + html << "" + else + html << "" + html << "" + end + + # Close preview container + html << "
    " - # Close preview container - html << "
    " end end @@ -86,7 +88,7 @@ module Details def detail(wrapper_options = nil) @detail ||= begin obj = options[:object] - + html = '
    ' - end end + end # Used when the preview is optional def has_detail? diff --git a/config/initializers/simple_form_custom.rb b/config/initializers/simple_form_custom.rb index 3e93c273..c36a4031 100644 --- a/config/initializers/simple_form_custom.rb +++ b/config/initializers/simple_form_custom.rb @@ -120,11 +120,60 @@ b.optional :readonly b.use :preview + + # ApplicationController.render( + # assigns: { builder: b }, + # template: 'binda/fieldable/_form_item_upload_file', + # layout: false + # ) + b.wrapper tag: 'div', class: 'fileupload--dashboard' do |bb| bb.use :label, class: 'control-label b-btn b-btn-primary', wrap_with: { tag: 'div', class: 'control-label-wrap' } bb.use :delete_button bb.use :detail end + + b.use :input + end + + config.wrappers :file_read_only do |b| + b.optional :error, wrap_with: { tag: 'span', class: 'help-block' } + b.wrapper tag: :p, class: 'help-block', unless_blank: true do |bb| + bb.optional :hint + end + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :readonly + + b.use :preview + + b.wrapper tag: 'div', class: 'fileupload--dashboard' do |bb| + bb.use :delete_button + bb.use :detail + end + + b.use :input + end + + config.wrappers :file_uploadable do |b| + b.optional :error, wrap_with: { tag: 'span', class: 'help-block' } + b.wrapper tag: :p, class: 'help-block', unless_blank: true do |bb| + bb.optional :hint + end + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :readonly + + b.use :preview + + b.wrapper tag: 'div', class: 'fileupload--dashboard' do |bb| + bb.use :label, class: 'control-label b-btn b-btn-primary', wrap_with: { tag: 'div', class: 'control-label-wrap test-label' } + bb.use :delete_button + bb.use :detail + end + b.use :input end @@ -139,7 +188,7 @@ ba.use :label_input end end - + config.wrappers :vertical_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| b.use :html5 b.optional :readonly @@ -168,7 +217,7 @@ config.wrapper_mappings = { check_boxes: :vertical_radio_and_checkboxes, radio_buttons: :vertical_radio_and_checkboxes, - file: :fileupload, + file: :file_uploadable, boolean: :vertical_boolean, datetime: :multi_select, date: :multi_select, diff --git a/config/locales/en.yml b/config/locales/en.yml index a0ba8b14..7138bc9a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -118,6 +118,8 @@ en: has_categories: Enable categories has_preview: Enable preview read_only: Read Only + preview: Preview + required: Required has_categories_hint: Enable a taxonomy specific to this structure preview_path_warning: Make preview available. Once enabled, remember to create a route in you application for relation_warning: This relation isn't set properly. Ensure the setup of this relation refers to at least one structure. @@ -160,3 +162,5 @@ en: no_available_choice_superadmin_hint: Please ensure you setup at least two possible choices when editing the settings: Settings duplicate_validation: There is already one %{arg1} associated to component '%{arg2}' and field setting '%{arg3}' + readonly_validation: > + %{arg2} can't be updated, this instance is read_only. Ref { type: %{arg1}, id: %{arg3} } \ No newline at end of file diff --git a/db/migrate/1_create_binda_tables.rb b/db/migrate/1_create_binda_tables.rb index ac35b02e..c0eecff5 100644 --- a/db/migrate/1_create_binda_tables.rb +++ b/db/migrate/1_create_binda_tables.rb @@ -49,6 +49,7 @@ def change t.integer :position t.boolean :required, default: false t.boolean :read_only, default: false + t.boolean :preview, default: false t.text :default_text t.string :field_type t.belongs_to :field_group diff --git a/db/migrate/20180828084441_add_has_preview_to_binda_field_settings.rb b/db/migrate/20180828084441_add_has_preview_to_binda_field_settings.rb new file mode 100644 index 00000000..d176cc33 --- /dev/null +++ b/db/migrate/20180828084441_add_has_preview_to_binda_field_settings.rb @@ -0,0 +1,5 @@ +class AddHasPreviewToBindaFieldSettings < ActiveRecord::Migration[5.1] + def change + add_column :binda_field_settings, :has_preview, :boolean, default: false + end +end diff --git a/lib/tasks/clean_dummy_app.rake b/lib/tasks/clean_dummy_app.rake new file mode 100644 index 00000000..6bcdc7d0 --- /dev/null +++ b/lib/tasks/clean_dummy_app.rake @@ -0,0 +1,31 @@ +namespace :binda do + + desc "Update dummy app migrations and schema" + task :update_dummy => :environment do + puts "---------------------" + puts "Remove current schema" + puts "---------------------" + sh "cd #{Rails.root} && rm -r db/schema.rb" + puts "" + puts "----------------------------------------------" + puts "Drop and create new dev db, then install Binda" + puts "----------------------------------------------" + sh "rails db:drop && rails db:create && rails generate binda:install" + puts "" + puts "--------------------------------------------------" + puts "Remove old dummy migrations and devise config file" + puts "--------------------------------------------------" + sh "rm -rf #{Rails.root}/db/migrate && rm -rf #{Rails.root}/config/initializers/devise_backup_*.rb && + cd #{Rails.root}/../.." + puts "" + puts "------------------------" + puts "Create new clean test db" + puts "------------------------" + sh "rails db:drop RAILS_ENV=test && rails db:create RAILS_ENV=test && rails db:migrate RAILS_ENV=test" + puts "" + puts "------------------------" + puts "Dummy updated" + puts "------------------------" + end + +end \ No newline at end of file diff --git a/package.json b/package.json index be0aa3e6..90f56303 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-preset-env": "^1.2.2", "babel-preset-es2015": "^6.24.0", + "cypress": "^3.1.1", "jshint": "^2.9.4", "jshint-loader": "^0.8.4", "webpack": "^3.10.0" diff --git a/spec/cypress.json b/spec/cypress.json new file mode 100644 index 00000000..fe0e205e --- /dev/null +++ b/spec/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:5002", + "defaultCommandTimeout": 10000 +} diff --git a/spec/cypress/app_commands/activerecord_fixtures.rb b/spec/cypress/app_commands/activerecord_fixtures.rb new file mode 100644 index 00000000..ff24f5bb --- /dev/null +++ b/spec/cypress/app_commands/activerecord_fixtures.rb @@ -0,0 +1,21 @@ +# you can delete this file if you don't use Rails Test Fixtures + +fixtures_dir = command_options.try(:[], 'fixtures_dir') +fixture_files = command_options.try(:[], 'fixtures') + +if defined?(ActiveRecord) + require "active_record/fixtures" + + fixtures_dir ||= ActiveRecord::Tasks::DatabaseTasks.fixtures_path + fixture_files ||= Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] } + + logger.debug "loading fixtures: { dir: #{fixtures_dir}, files: #{fixture_files} }" + ActiveRecord::FixtureSet.reset_cache + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) +else # this else part can be removed + logger.error "Looks like activerecord_fixtures has to be modified to suite your need" + Post.create(title: 'MyCypressFixtures') + Post.create(title: 'MyCypressFixtures2') + Post.create(title: 'MyRailsFixtures') + Post.create(title: 'MyRailsFixtures2') +end diff --git a/spec/cypress/app_commands/clean.rb b/spec/cypress/app_commands/clean.rb new file mode 100644 index 00000000..ce8c69e0 --- /dev/null +++ b/spec/cypress/app_commands/clean.rb @@ -0,0 +1,8 @@ +if defined?(DatabaseCleaner) + # cleaning the database using database_cleaner + DatabaseCleaner.strategy = :truncation + DatabaseCleaner.clean +else + logger.warn "add database_cleaner or update clean_db" + Post.delete_all if defined?(Post) +end diff --git a/spec/cypress/app_commands/eval.rb b/spec/cypress/app_commands/eval.rb new file mode 100644 index 00000000..3a39bf3e --- /dev/null +++ b/spec/cypress/app_commands/eval.rb @@ -0,0 +1 @@ +Kernel.eval(command_options) unless command_options.nil? diff --git a/spec/cypress/app_commands/factory_bot.rb b/spec/cypress/app_commands/factory_bot.rb new file mode 100644 index 00000000..04525ecc --- /dev/null +++ b/spec/cypress/app_commands/factory_bot.rb @@ -0,0 +1,12 @@ +Array.wrap(command_options).each do |factory_options| + factory_method = factory_options.shift + begin + logger.debug "running #{factory_method}, #{factory_options}" + CypressDev::SmartFactoryWrapper.public_send(factory_method, *factory_options) + rescue => e + logger.error "#{e.class}: #{e.message}" + logger.error e.backtrace.join("\n") + logger.error "#{e.record.inspect}" if e.is_a?(ActiveRecord::RecordInvalid) + raise e + end +end diff --git a/spec/cypress/app_commands/scenarios/basic.rb b/spec/cypress/app_commands/scenarios/basic.rb new file mode 100644 index 00000000..2c1fd35d --- /dev/null +++ b/spec/cypress/app_commands/scenarios/basic.rb @@ -0,0 +1,3 @@ +# You can setup your Rails state here +# MyModel.create name: 'something' +Post.create(title: 'I am a Postman') diff --git a/spec/cypress/cypress_helper.rb b/spec/cypress/cypress_helper.rb new file mode 100644 index 00000000..b3a8cd72 --- /dev/null +++ b/spec/cypress/cypress_helper.rb @@ -0,0 +1,30 @@ +# This is loaded once before the first command is executed + +begin + require 'database_cleaner' +rescue LoadError => e + puts e.message +end + +begin + require 'factory_bot_rails' +rescue LoadError => e + puts e.message + begin + require 'factory_girl_rails' + rescue LoadError => e + puts e.message + end +end + +require 'cypress_dev/smart_factory_wrapper' + +factory = CypressDev::SimpleRailsFactory +factory = FactoryBot if defined?(FactoryBot) +factory = FactoryGirl if defined?(FactoryGirl) + +CypressDev::SmartFactoryWrapper.configure( + always_reload: !Rails.configuration.cache_classes, + factory: factory, + files: %w(spec/factories.rb ./spec/factories/**/*.rb) +) diff --git a/spec/cypress/fixtures/example.json b/spec/cypress/fixtures/example.json new file mode 100644 index 00000000..da18d935 --- /dev/null +++ b/spec/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/spec/cypress/integration/examples/actions.spec.js b/spec/cypress/integration/examples/actions.spec.js new file mode 100644 index 00000000..0375aba7 --- /dev/null +++ b/spec/cypress/integration/examples/actions.spec.js @@ -0,0 +1,272 @@ +/// + +context('Actions', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/actions') + }) + + // https://on.cypress.io/interacting-with-elements + + it('.type() - type into a DOM element', () => { + // https://on.cypress.io/type + cy.get('.action-email') + .type('fake@email.com').should('have.value', 'fake@email.com') + + // .type() with special character sequences + .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') + .type('{del}{selectall}{backspace}') + + // .type() with key modifiers + .type('{alt}{option}') //these are equivalent + .type('{ctrl}{control}') //these are equivalent + .type('{meta}{command}{cmd}') //these are equivalent + .type('{shift}') + + // Delay each keypress by 0.1 sec + .type('slow.typing@email.com', { delay: 100 }) + .should('have.value', 'slow.typing@email.com') + + cy.get('.action-disabled') + // Ignore error checking prior to type + // like whether the input is visible or disabled + .type('disabled error checking', { force: true }) + .should('have.value', 'disabled error checking') + }) + + it('.focus() - focus on a DOM element', () => { + // https://on.cypress.io/focus + cy.get('.action-focus').focus() + .should('have.class', 'focus') + .prev().should('have.attr', 'style', 'color: orange;') + }) + + it('.blur() - blur off a DOM element', () => { + // https://on.cypress.io/blur + cy.get('.action-blur').type('About to blur').blur() + .should('have.class', 'error') + .prev().should('have.attr', 'style', 'color: red;') + }) + + it('.clear() - clears an input or textarea element', () => { + // https://on.cypress.io/clear + cy.get('.action-clear').type('Clear this text') + .should('have.value', 'Clear this text') + .clear() + .should('have.value', '') + }) + + it('.submit() - submit a form', () => { + // https://on.cypress.io/submit + cy.get('.action-form') + .find('[type="text"]').type('HALFOFF') + cy.get('.action-form').submit() + .next().should('contain', 'Your form has been submitted!') + }) + + it('.click() - click on a DOM element', () => { + // https://on.cypress.io/click + cy.get('.action-btn').click() + + // You can click on 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- + + // clicking in the center of the element is the default + cy.get('#action-canvas').click() + + cy.get('#action-canvas').click('topLeft') + cy.get('#action-canvas').click('top') + cy.get('#action-canvas').click('topRight') + cy.get('#action-canvas').click('left') + cy.get('#action-canvas').click('right') + cy.get('#action-canvas').click('bottomLeft') + cy.get('#action-canvas').click('bottom') + cy.get('#action-canvas').click('bottomRight') + + // .click() accepts an x and y coordinate + // that controls where the click occurs :) + + cy.get('#action-canvas') + .click(80, 75) // click 80px on x coord and 75px on y coord + .click(170, 75) + .click(80, 165) + .click(100, 185) + .click(125, 190) + .click(150, 185) + .click(170, 165) + + // click multiple elements by passing multiple: true + cy.get('.action-labels>.label').click({ multiple: true }) + + // Ignore error checking prior to clicking + cy.get('.action-opacity>.btn').click({ force: true }) + }) + + it('.dblclick() - double click on a DOM element', () => { + // https://on.cypress.io/dblclick + + // Our app has a listener on 'dblclick' event in our 'scripts.js' + // that hides the div and shows an input on double click + cy.get('.action-div').dblclick().should('not.be.visible') + cy.get('.action-input-hidden').should('be.visible') + }) + + it('.check() - check a checkbox or radio element', () => { + // https://on.cypress.io/check + + // By default, .check() will check all + // matching checkbox or radio elements in succession, one after another + cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') + .check().should('be.checked') + + cy.get('.action-radios [type="radio"]').not('[disabled]') + .check().should('be.checked') + + // .check() accepts a value argument + cy.get('.action-radios [type="radio"]') + .check('radio1').should('be.checked') + + // .check() accepts an array of values + cy.get('.action-multiple-checkboxes [type="checkbox"]') + .check(['checkbox1', 'checkbox2']).should('be.checked') + + // Ignore error checking prior to checking + cy.get('.action-checkboxes [disabled]') + .check({ force: true }).should('be.checked') + + cy.get('.action-radios [type="radio"]') + .check('radio3', { force: true }).should('be.checked') + }) + + it('.uncheck() - uncheck a checkbox element', () => { + // https://on.cypress.io/uncheck + + // By default, .uncheck() will uncheck all matching + // checkbox elements in succession, one after another + cy.get('.action-check [type="checkbox"]') + .not('[disabled]') + .uncheck().should('not.be.checked') + + // .uncheck() accepts a value argument + cy.get('.action-check [type="checkbox"]') + .check('checkbox1') + .uncheck('checkbox1').should('not.be.checked') + + // .uncheck() accepts an array of values + cy.get('.action-check [type="checkbox"]') + .check(['checkbox1', 'checkbox3']) + .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') + + // Ignore error checking prior to unchecking + cy.get('.action-check [disabled]') + .uncheck({ force: true }).should('not.be.checked') + }) + + it('.select() - select an option in a