diff options
8 files changed, 372 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index b424e7f205d..50c725aa3d5 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate';  import intervalPatternInput from './components/interval_pattern_input.vue';  import TimezoneDropdown from './components/timezone_dropdown';  import TargetBranchDropdown from './components/target_branch_dropdown'; +import { setupPipelineVariableList } from './setup_pipeline_variable_list';  Vue.use(Translate); @@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {    gl.timezoneDropdown = new TimezoneDropdown();    gl.targetBranchDropdown = new TargetBranchDropdown();    gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + +  setupPipelineVariableList($('.js-pipeline-variable-list'));  }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js new file mode 100644 index 00000000000..644efd10509 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -0,0 +1,71 @@ +function insertRow($row) { +  const $rowClone = $row.clone(); +  $rowClone.removeAttr('data-is-persisted'); +  $rowClone.find('input, textarea').val(''); +  $row.after($rowClone); +} + +function removeRow($row) { +  const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + +  if (isPersisted) { +    $row.hide(); +    $row +      .find('.js-destroy-input') +      .val(1); +  } else { +    $row.remove(); +  } +} + +function checkIfRowTouched($row) { +  return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); +} + +function setupPipelineVariableList(parent = document) { +  const $parent = $(parent); + +  $parent.on('click', '.js-row-remove-button', (e) => { +    const $row = $(e.currentTarget).closest('.js-row'); +    removeRow($row); + +    e.preventDefault(); +  }); + +  // Remove any empty rows except the last r +  $parent.on('blur', '.js-user-input', (e) => { +    const $row = $(e.currentTarget).closest('.js-row'); + +    const isTouched = checkIfRowTouched($row); +    if ($row.is(':not(:last-child)') && !isTouched) { +      removeRow($row); +    } +  }); + +  // Always make sure there is an empty last row +  $parent.on('input', '.js-user-input', () => { +    const $lastRow = $parent.find('.js-row').last(); + +    const isTouched = checkIfRowTouched($lastRow); +    if (isTouched) { +      insertRow($lastRow); +    } +  }); + +  // Clear out the empty last row so it +  // doesn't get submitted and throw validation errors +  $parent.closest('form').on('submit', () => { +    const $lastRow = $parent.find('.js-row').last(); + +    const isTouched = checkIfRowTouched($lastRow); +    if (!isTouched) { +      $lastRow.find('input, textarea').attr('name', ''); +    } +  }); +} + +export { +  setupPipelineVariableList, +  insertRow, +  removeRow, +}; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index da4d91511e0..a1a09b20548 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -575,6 +575,12 @@ $stage-hover-border: #d1e7fc;  $action-icon-color: #d6d6d6;  /* +Pipeline Schedules +*/ +$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); + + +/*  Filtered Search  */  $filter-name-resting-color: #f8f8f8; diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 595eb40fec7..b3743a7c88d 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -74,3 +74,65 @@      margin-right: 3px;    }  } + +.pipeline-variable-list { +  margin-left: 0; +  margin-bottom: 0; +  padding-left: 0; +} + +.pipeline-variable-row { +  display: flex; + +  &:not(:last-child) { +    margin-bottom: $gl-btn-padding; +  } + +  @media (max-width: $screen-xs-max) { +    flex-wrap: wrap; +  } + +  &:last-child { +    & > .pipeline-variable-row-remove-button { +      display: none; +    } + +    & > .pipeline-variable-value-input { +      margin-right: $pipeline-variable-remove-button-width; +    } +  } +} + +.pipeline-variable-key-input { +  margin-right: $gl-btn-padding; + +  @media (max-width: $screen-xs-max) { +    margin-right: $pipeline-variable-remove-button-width; +    margin-bottom: $gl-btn-padding; +  } +} + +.pipeline-variable-value-input { +  @media (max-width: $screen-xs-max) { +    flex: 1; +  } +} + +.pipeline-variable-row-remove-button { +  flex-shrink: 0; +  display: flex; +  justify-content: center; +  align-items: center; +  width: $pipeline-variable-remove-button-width; +  padding: 0; +  background: transparent; +  border: 0; +  color: $gl-text-color-secondary; +  @include transition(color); + +  &:hover, +  &:focus { +    outline: none; +    color: $gl-text-color; +  } +} diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index fc7fa5c1876..4f65532e279 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -24,6 +24,15 @@        = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true    .form-group      .col-md-9 +      %label.label-light +        #{ _('Variables') } +      %ul.js-pipeline-variable-list.pipeline-variable-list +        - if @schedule.variables.present? +          - @schedule.variables.each_with_index do |variable, i| +            = render 'variable_row', id: variable.id, key: variable.key, value: variable.value +        = render 'variable_row' +  .form-group +    .col-md-9        = f.label  :active, s_('PipelineSchedules|Activated'), class: 'label-light'        %div          = f.check_box :active, required: false, value: @schedule.active? diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml new file mode 100644 index 00000000000..85813b2ffd4 --- /dev/null +++ b/app/views/projects/pipeline_schedules/_variable_row.html.haml @@ -0,0 +1,16 @@ +- id = local_assigns.fetch(:id, nil) +- key = local_assigns.fetch(:key, "") +- value = local_assigns.fetch(:value, "") +%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } } +  %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id } +  %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" } +  %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text", +    name: "schedule[variables_attributes][][key]", +    value: key, +    placeholder: _('Input variable key') } +  %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1, +    name: "schedule[variables_attributes][][value]", +    placeholder: _('Input variable value') } +    = value +  %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': _('Remove variable row') } +    %i.fa.fa-minus-circle{ 'aria-hidden': "true" } diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index dfb973c37e5..0adc192b804 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -98,6 +98,15 @@ feature 'Pipeline Schedules', :feature do        expect(page).to have_content('This field is required')      end + +    it 'sets a variable' do +      fill_in_schedule_form +      fill_in_variable + +      save_pipeline_schedule + +      expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }]) +    end    end    describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do @@ -120,6 +129,14 @@ feature 'Pipeline Schedules', :feature do        expect(page).to have_content('my brand new description')      end +    it 'adds a new variable' do +      fill_in_variable + +      save_pipeline_schedule + +      expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }]) +    end +      context 'when ref is nil' do        before do          pipeline_schedule.update_attribute(:ref, nil) @@ -132,6 +149,40 @@ feature 'Pipeline Schedules', :feature do          end        end      end + +    context 'when variables already exist' do +      before do +        create(:ci_pipeline_schedule_variable, key: 'some_key', value: 'some_value', pipeline_schedule: pipeline_schedule) +        edit_pipeline_schedule +      end + +      it 'edits existing variable' do +        expect(first('[name="schedule[variables_attributes][][key]"]').value).to eq('some_key') +        expect(first('[name="schedule[variables_attributes][][value]"]').value).to eq('some_value') + +        fill_in_variable +        save_pipeline_schedule + +        expect(Ci::PipelineSchedule.last.job_variables).to eq([{ key: 'foo', value: 'bar', public: false }]) +      end + +      it 'removes an existing variable' do +        remove_variable +        save_pipeline_schedule + +        expect(Ci::PipelineSchedule.last.job_variables).to eq([]) +      end + +      it 'adds another variable' do +        fill_in_variable(1) +        save_pipeline_schedule + +        expect(Ci::PipelineSchedule.last.job_variables).to eq([ +          { key: 'some_key', value: 'some_value', public: false }, +          { key: 'foo', value: 'bar', public: false } +        ]) +      end +    end    end    def visit_new_pipeline_schedule @@ -160,6 +211,15 @@ feature 'Pipeline Schedules', :feature do      click_button 'Save pipeline schedule'    end +  def fill_in_variable(index = 0) +    all('[name="schedule[variables_attributes][][key]"]')[index].set('foo') +    all('[name="schedule[variables_attributes][][value]"]')[index].set('bar') +  end + +  def remove_variable +    first('.js-pipeline-variable-list .js-row-remove-button').click +  end +    def fill_in_schedule_form      fill_in 'schedule_description', with: 'my fancy description'      fill_in 'schedule_cron', with: '* 1 2 3 4' diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js new file mode 100644 index 00000000000..5b316b319a5 --- /dev/null +++ b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js @@ -0,0 +1,145 @@ +import { +  setupPipelineVariableList, +  insertRow, +  removeRow, +} from '~/pipeline_schedules/setup_pipeline_variable_list'; + +describe('Pipeline Variable List', () => { +  let $markup; + +  describe('insertRow', () => { +    it('should insert another row', () => { +      $markup = $(`<div> +        <li class="js-row"> +          <input> +          <textarea></textarea> +        </li> +      </div>`); + +      insertRow($markup.find('.js-row')); + +      expect($markup.find('.js-row').length).toBe(2); +    }); + +    it('should clear `data-is-persisted` on cloned row', () => { +      $markup = $(`<div> +        <li class="js-row" data-is-persisted="true"></li> +      </div>`); + +      insertRow($markup.find('.js-row')); + +      const $lastRow = $markup.find('.js-row').last(); +      expect($lastRow.attr('data-is-persisted')).toBe(undefined); +    }); + +    it('should clear inputs on cloned row', () => { +      $markup = $(`<div> +        <li class="js-row"> +          <input value="foo"> +          <textarea>bar</textarea> +        </li> +      </div>`); + +      insertRow($markup.find('.js-row')); + +      const $lastRow = $markup.find('.js-row').last(); +      expect($lastRow.find('input').val()).toBe(''); +      expect($lastRow.find('textarea').val()).toBe(''); +    }); +  }); + +  describe('removeRow', () => { +    it('should remove dynamic row', () => { +      $markup = $(`<div> +        <li class="js-row"> +          <input> +          <textarea></textarea> +        </li> +      </div>`); + +      removeRow($markup.find('.js-row')); + +      expect($markup.find('.js-row').length).toBe(0); +    }); + +    it('should hide and mark to destroy with already persisted rows', () => { +      $markup = $(`<div> +        <li class="js-row" data-is-persisted="true"> +          <input class="js-destroy-input"> +        </li> +      </div>`); + +      const $row = $markup.find('.js-row'); +      removeRow($row); + +      expect($row.find('.js-destroy-input').val()).toBe('1'); +      expect($markup.find('.js-row').length).toBe(1); +    }); +  }); + +  describe('setupPipelineVariableList', () => { +    beforeEach(() => { +      $markup = $(`<form> +        <li class="js-row"> +          <input class="js-user-input" name="schedule[variables_attributes][][key]"> +          <textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea> +          <button class="js-row-remove-button"></button> +          <button class="js-row-add-button"></button> +        </li> +      </form>`); + +      setupPipelineVariableList($markup); +    }); + +    it('should remove the row when clicking the remove button', () => { +      $markup.find('.js-row-remove-button').trigger('click'); + +      expect($markup.find('.js-row').length).toBe(0); +    }); + +    it('should add another row when editing the last rows key input', () => { +      const $row = $markup.find('.js-row'); +      $row.find('input.js-user-input') +        .val('foo') +        .trigger('input'); + +      expect($markup.find('.js-row').length).toBe(2); +    }); + +    it('should add another row when editing the last rows value textarea', () => { +      const $row = $markup.find('.js-row'); +      $row.find('textarea.js-user-input') +        .val('foo') +        .trigger('input'); + +      expect($markup.find('.js-row').length).toBe(2); +    }); + +    it('should remove empty row after blurring', () => { +      const $row = $markup.find('.js-row'); +      $row.find('input.js-user-input') +        .val('foo') +        .trigger('input'); + +      expect($markup.find('.js-row').length).toBe(2); + +      $row.find('input.js-user-input') +        .val('') +        .trigger('input') +        .trigger('blur'); + +      expect($markup.find('.js-row').length).toBe(1); +    }); + +    it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { +      const $row = $markup.find('.js-row'); +      expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]'); +      expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]'); + +      $markup.filter('form').submit(); + +      expect($row.find('input').attr('name')).toBe(''); +      expect($row.find('textarea').attr('name')).toBe(''); +    }); +  }); +});  | 
