diff options
author | Eric Eastwood <contact@ericeastwood.com> | 2017-06-19 10:59:10 -0500 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-07-05 18:36:19 +0900 |
commit | 5576214d0fbbc8b7f208367e3eedd6347b21151b (patch) | |
tree | 3526e353bfa743033c343faacdbdfebc511d3772 | |
parent | d7cd3c3635bc6200cd9c8668a025826818f19a80 (diff) | |
download | gitlab-ce-5576214d0fbbc8b7f208367e3eedd6347b21151b.tar.gz |
Schedule pipelines with variables
Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32568
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(''); + }); + }); +}); |