diff --git a/app/frontend/js/components/Edit/TrixField.vue b/app/frontend/js/components/Edit/TrixField.vue new file mode 100644 index 0000000000..c46dab9a43 --- /dev/null +++ b/app/frontend/js/components/Edit/TrixField.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/app/frontend/js/components/Show/TrixField.vue b/app/frontend/js/components/Show/TrixField.vue new file mode 100644 index 0000000000..9ce7663841 --- /dev/null +++ b/app/frontend/js/components/Show/TrixField.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/app/frontend/js/components/index.js b/app/frontend/js/components/index.js index 9a6f9e995a..6026e54486 100644 --- a/app/frontend/js/components/index.js +++ b/app/frontend/js/components/index.js @@ -48,6 +48,7 @@ Vue.component('show-country-field', require('@/js/components/Sh Vue.component('show-badge-field', require('@/js/components/Show/BadgeField.vue').default) Vue.component('show-heading-field', require('@/js/components/Show/HeadingField.vue').default) Vue.component('show-code-field', require('@/js/components/Show/CodeField.vue').default) +Vue.component('show-trix-field', require('@/js/components/Show/TrixField.vue').default) Vue.component('edit-field-wrapper', require('@/js/components/Edit/FieldWrapper.vue').default) Vue.component('edit-id-field', require('@/js/components/Edit/IdField.vue').default) @@ -70,6 +71,7 @@ Vue.component('edit-country-field', require('@/js/components/Ed Vue.component('edit-heading-field', require('@/js/components/Show/HeadingField.vue').default) Vue.component('edit-code-field', require('@/js/components/Edit/CodeField.vue').default) Vue.component('edit-hidden-field', require('@/js/components/Edit/HiddenField.vue').default) +Vue.component('edit-trix-field', require('@/js/components/Edit/TrixField.vue').default) // Form Fields Vue.component('input-component', require('@/js/components/InputComponent.vue').default) diff --git a/lib/avo/app/fields/trix_field.rb b/lib/avo/app/fields/trix_field.rb new file mode 100644 index 0000000000..660799538a --- /dev/null +++ b/lib/avo/app/fields/trix_field.rb @@ -0,0 +1,25 @@ +require_relative 'field' + +module Avo + module Fields + class TrixField < Field + def initialize(name, **args, &block) + @defaults = { + component: 'trix-field', + } + + super(name, **args, &block) + + hide_on :index + + @always_show = args[:always_show].present? ? args[:always_show] : false + end + + def hydrate_field(fields, model, resource, view) + { + always_show: @always_show + } + end + end + end +end diff --git a/lib/avo/app/resource_fields.rb b/lib/avo/app/resource_fields.rb index e40be35990..5ac1ed53d3 100644 --- a/lib/avo/app/resource_fields.rb +++ b/lib/avo/app/resource_fields.rb @@ -16,6 +16,10 @@ def get_fields def add_field(resource, field) @@fields[resource].push field end + + def trix(name, **args, &block) + @@fields[self].push Avo::Fields::TrixField::new(name, **args, &block) + end end end end diff --git a/package.json b/package.json index aa73a5cb05..d36759da51 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@rails/activestorage": "^6.0.2-1", "@rails/webpacker": "^4.2.2", + "@tailwindcss/typography": "^0.2.0", "autoprefixer": "^9.7.4", "axios": "^0.19.2", "css-loader": "^3.5.3", @@ -58,6 +59,7 @@ "vue-svg-loader": "^0.16.0", "vue-template-compiler": "^2.6.11", "vue-toasted": "^1.1.28", + "vue-trix": "^1.1.11", "vue-turbolinks": "^2.1.0", "vuejs-paginate": "^2.1.0", "vuex": "^3.5.1", diff --git a/spec/dummy/app/avo/resources/post.rb b/spec/dummy/app/avo/resources/post.rb index 3cfe06ea9a..b4ecb5df5a 100644 --- a/spec/dummy/app/avo/resources/post.rb +++ b/spec/dummy/app/avo/resources/post.rb @@ -11,13 +11,14 @@ def initialize fields do id text :name, required: true - textarea :body, nullable: true, null_values: ['0', '', 'null', 'nil'], format_using: -> (value) { value.to_s.truncate 100 } + trix :body, placeholder: 'Enter text', always_show: false file :cover_photo, is_image: true boolean :is_featured boolean :is_published do |model| model.published_at.present? end + belongs_to :user, searchable: false, placeholder: '—' end diff --git a/spec/dummy/app/avo/resources/team.rb b/spec/dummy/app/avo/resources/team.rb index 5ee8042cbf..80d00d8062 100644 --- a/spec/dummy/app/avo/resources/team.rb +++ b/spec/dummy/app/avo/resources/team.rb @@ -9,7 +9,7 @@ def initialize fields do id text :name - textarea :description, rows: 5, readonly: false, hide_on: :index, format_using: -> (value) { value.to_s.truncate 30 }, required: true, default: 'This team is wonderful!' + textarea :description, rows: 5, readonly: false, hide_on: :index, format_using: -> (value) { value.to_s.truncate 30 }, default: 'This team is wonderful!', nullable: true, null_values: ['0', '', 'null', 'nil'] number :members_count do |model| model.members.count diff --git a/spec/system/avo/nullable_field_spec.rb b/spec/system/avo/nullable_field_spec.rb index 750146c3f8..eadbb10e46 100644 --- a/spec/system/avo/nullable_field_spec.rb +++ b/spec/system/avo/nullable_field_spec.rb @@ -2,76 +2,76 @@ RSpec.describe 'NullableField', type: :system do describe 'without input (specifying null_values: ["", "0", "null", "nil"])' do - let!(:post) { create :post, body: nil } + let!(:team) { create :team, description: nil } context 'show' do - it 'displays the posts empty body (dash)' do - visit "/avo/resources/post/#{post.id}" + it 'displays the teams empty description (dash)' do + visit "/avo/resources/teams/#{team.id}" - expect(find_field_value_element('body')).to have_text empty_dash + expect(find_field_value_element('description')).to have_text empty_dash end end context 'edit' do - it 'has the posts body empty' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'has the teams description empty' do + visit "/avo/resources/teams/#{team.id}/edit" - expect(find_field('body').value).to eq '' + expect(find_field('description').value).to eq '' end end end describe 'with regular input (specifying null_values: ["", "0", "null", "nil"])' do - let!(:post) { create :post, body: 'descr' } + let!(:team) { create :team, description: 'descr' } context 'edit' do - it 'has the posts body prefilled' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'has the teams description prefilled' do + visit "/avo/resources/teams/#{team.id}/edit" - expect(find_field('body').value).to eq 'descr' + expect(find_field('description').value).to eq 'descr' end - it 'changes the posts body to null ("" - empty string)' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'changes the teams description to null ("" - empty string)' do + visit "/avo/resources/teams/#{team.id}/edit" - fill_in 'body', with: '' + fill_in 'description', with: '' click_on 'Save' wait_for_loaded - expect(current_path).to eql "/avo/resources/posts/#{post.id}" - expect(find_field_value_element('body')).to have_text empty_dash + expect(current_path).to eql "/avo/resources/teams/#{team.id}" + expect(find_field_value_element('description')).to have_text empty_dash end - it 'changes the posts body to null ("0")' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'changes the teams description to null ("0")' do + visit "/avo/resources/teams/#{team.id}/edit" - fill_in 'body', with: '0' + fill_in 'description', with: '0' click_on 'Save' wait_for_loaded - expect(current_path).to eql "/avo/resources/posts/#{post.id}" - expect(find_field_value_element('body')).to have_text empty_dash + expect(current_path).to eql "/avo/resources/teams/#{team.id}" + expect(find_field_value_element('description')).to have_text empty_dash end - it 'changes the posts body to null ("nil")' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'changes the teams description to null ("nil")' do + visit "/avo/resources/teams/#{team.id}/edit" - fill_in 'body', with: 'nil' + fill_in 'description', with: 'nil' click_on 'Save' wait_for_loaded - expect(current_path).to eql "/avo/resources/posts/#{post.id}" - expect(find_field_value_element('body')).to have_text empty_dash + expect(current_path).to eql "/avo/resources/teams/#{team.id}" + expect(find_field_value_element('description')).to have_text empty_dash end - it 'changes the postss body to null ("null")' do - visit "/avo/resources/posts/#{post.id}/edit" + it 'changes the teams description to null ("null")' do + visit "/avo/resources/teams/#{team.id}/edit" - fill_in 'body', with: 'null' + fill_in 'description', with: 'null' click_on 'Save' wait_for_loaded - expect(current_path).to eql "/avo/resources/posts/#{post.id}" - expect(find_field_value_element('body')).to have_text empty_dash + expect(current_path).to eql "/avo/resources/teams/#{team.id}" + expect(find_field_value_element('description')).to have_text empty_dash end end end diff --git a/spec/system/avo/trix_field_spec.rb b/spec/system/avo/trix_field_spec.rb new file mode 100644 index 0000000000..40335c4f13 --- /dev/null +++ b/spec/system/avo/trix_field_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +RSpec.describe 'TrixField', type: :system do + describe 'without value' do + let!(:post) { create :post , body: '' } + + context 'show' do + it 'displays the posts empty body (dash)' do + visit "/avo/resources/posts/#{post.id}" + + expect(find_field_element('body')).to have_text empty_dash + end + end + + context 'edit' do + it 'has the posts body label and empty trix editor and placeholder' do + visit "/avo/resources/posts/#{post.id}/edit" + + body_element = find_field_element('body') + + expect(body_element).to have_text 'Body' + + expect(find(:xpath, "//trix-editor[@input='trixEditor']")[:placeholder]).to have_text('Enter text') + expect(find_trix_editor('trixEditor')).to have_text('') + end + + it 'change the posts body text' do + visit "/avo/resources/posts/#{post.id}/edit" + + fill_in_trix_editor 'trixEditor', with: 'Works for us!!!' + + click_on 'Save' + wait_for_loaded + + click_on 'Show Content' + + expect(find_field_value_element('body')).to have_text 'Works for us!!!' + end + end + end + + describe 'with regular value' do + let!(:body) { 'Example trix text.' } + let!(:post) { create :post , body: body } + + context 'show' do + it 'displays the posts body' do + visit "/avo/resources/posts/#{post.id}" + + click_on 'Show Content' + + expect(find_field_value_element('body')).to have_text body + end + end + + context 'edit' do + it 'has the posts body label' do + visit "/avo/resources/posts/#{post.id}/edit" + + body_element = find_field_element('body') + + expect(body_element).to have_text 'Body' + end + + it 'has filled simple text in trix editor' do + visit "/avo/resources/posts/#{post.id}/edit" + + expect(find_trix_editor('trixEditor').value).to eq('
' + body + '
') + end + + it 'change the posts body trix to another simple text value' do + visit "/avo/resources/posts/#{post.id}/edit" + + fill_in_trix_editor 'trixEditor', with: 'New example!' + + click_on 'Save' + wait_for_loaded + + click_on 'Show Content' + + expect(find_field_value_element('body')).to have_text 'New example!' + end + end + end + + def fill_in_trix_editor(id, with:) + find(:xpath, "//trix-editor[@input='#{id}']").click.set(with) + end + + def find_trix_editor(id) + find(:xpath, "//*[@id='#{id}']", visible: false) + end +end diff --git a/tailwind.config.js b/tailwind.config.js index cac295ee12..98f246d4f5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -77,6 +77,7 @@ module.exports = { cursor: ['responsive', 'disabled'], }, plugins: [ + require('@tailwindcss/typography'), // buttons plugin(({ addComponents, theme }) => { const styles = { diff --git a/yarn.lock b/yarn.lock index 664b07008d..ed58d1a39a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -863,6 +863,11 @@ webpack-cli "^3.3.10" webpack-sources "^1.4.3" +"@tailwindcss/typography@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.2.0.tgz#b597c83502e3c3c6641a8aaabda223cd494ab349" + integrity sha512-aPgMH+CjQiScLZculoDNOQUrrK2ktkbl3D6uCLYp1jgYRlNDrMONu9nMu8LfwAeetYNpVNeIGx7WzHSu0kvECg== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -7968,6 +7973,11 @@ trim-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= +trix@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/trix/-/trix-1.2.4.tgz#8b8f8ea58c2605ad3d04a3b82d60a994576e33a5" + integrity sha512-S6YUUaaHngNbW1jzBV3MQ35pisTV/x5yKlNZrAf8exkOnHsTj+2OJHx2jBLqRQEtpWHpg85pHjo2A7dKK2RQbQ== + "true-case-path@^1.0.2": version "1.0.3" resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" @@ -8373,6 +8383,13 @@ vue-toasted@^1.1.28: resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26" integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw== +vue-trix@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/vue-trix/-/vue-trix-1.1.11.tgz#02a7e00739c7c5243a90fe5927480a192484ba65" + integrity sha512-+5tY9ocpCpQ61qeJpaYEvQDvMb3kE5K33k51YiZna/lFlpLoas4lZAvZwBkvwf7TjfG9Kb5kh0FKNiqpSs7ARQ== + dependencies: + trix "^1.2.3" + vue-turbolinks@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vue-turbolinks/-/vue-turbolinks-2.1.0.tgz#fc7372d06d624aa3fb68231dc0b509719edc778f"