summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Eastwood <contact@ericeastwood.com>2017-06-19 10:59:10 -0500
committerEric Eastwood <contact@ericeastwood.com>2017-06-27 12:01:35 -0500
commitaeb31904336a45eaa76641c0d5ecb17e68e24c42 (patch)
tree8c1a863226e4e2f7abe2e41b9415a9e3fb4ad972
parent1dc1a7e9cea4eb42fef94a1cd571b91c5da404ee (diff)
downloadgitlab-ce-32568-schedule-pipeline-with-variables-with-nested-form-gem.tar.gz
WIP: Schedule pipelines with variables with nested_form gem32568-schedule-pipeline-with-variables-with-nested-form-gem
Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32568
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss74
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml51
-rw-r--r--vendor/assets/javascripts/jquery_nested_form.js120
6 files changed, 216 insertions, 33 deletions
diff --git a/Gemfile b/Gemfile
index 42b75af3a5c..f98231ad08d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -251,6 +251,7 @@ gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
+gem 'nested_form', '~> 0.3.2'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index d77ba37f16f..2499b0d12cf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -474,6 +474,7 @@ GEM
mustermann-grape (0.4.0)
mustermann (= 0.4.0)
mysql2 (0.3.20)
+ nested_form (0.3.2)
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
@@ -1017,6 +1018,7 @@ DEPENDENCIES
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
+ nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4)
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index b53f6284afc..9f3554bfa63 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -8,4 +8,5 @@ import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'vendor/jquery.nicescroll';
import 'vendor/jquery.waitforimages';
+import 'vendor/jquery_nested_form';
import 'select2/select2';
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 595eb40fec7..234dad202bf 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -74,3 +74,77 @@
margin-right: 3px;
}
}
+
+.pipeline-variable-list {
+ margin-left: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+}
+
+.pipeline-variable-row {
+ display: flex;
+ margin-bottom: $gl-btn-padding;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-btn-padding;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
+
+ & > .pipeline-variable-row-add-button {
+ display: none;
+ }
+
+ &:last-of-type {
+ & > .pipeline-variable-row-remove-button {
+ display: none;
+ }
+
+ & > .pipeline-variable-row-add-button {
+ display: inline-flex;
+ }
+ }
+}
+
+.pipeline-variable-key-input {
+ margin-right: $gl-btn-padding;
+
+ @media (max-width: $screen-xs-max) {
+ margin-right: calc(1em + #{2 * $gl-padding});
+ margin-bottom: $gl-btn-padding;
+ }
+}
+
+.pipeline-variable-value-input {
+ @media (max-width: $screen-xs-max) {
+ flex: 1;
+ }
+}
+
+.pipeline-variable-row-remove-button {
+ display: inline-flex;
+ align-items: center;
+ padding: 0 $gl-padding;
+ background: transparent;
+ border: 0;
+ color: $gl-text-color-secondary;
+
+ transition: color $general-hover-transition-duration $general-hover-transition-curve;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ color: $gl-text-color;
+ }
+
+ & > .fa {
+ width: 1em;
+ }
+}
+
+.pipeline-variable-row-add-button {
+ @extend .pipeline-variable-row-remove-button;
+}
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index c079d5712e3..fc763df2e53 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -2,7 +2,7 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedule_form'
-= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
+= nested_form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
.col-md-9
@@ -22,38 +22,23 @@
= f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
- -# TODO: Test code
- = @schedule.variables.inspect
- - if @schedule.variables.present?
- - @schedule.variables.each_with_index do |variable, i|
- .form-group
- .col-md-9
- %label.label-light Key
- %input.form-control{:name => "schedule[variables_attributes][#{i}][key]", :type => "text", :value => variable.key}/
- %p.gl-field-error.hide This field is required.
- %label.label-light Value
- %input.form-control{:name => "schedule[variables_attributes][#{i}][value]", :type => "text", :value => variable.value}/
- %p.gl-field-error.hide This field is required.
- - if @schedule.variables.count == 1
- - (1..1).each do |i|
- .form-group
- .col-md-9
- %label.label-light Key
- %input.form-control{:name => "schedule[variables_attributes][#{i}][key]", :type => "text"}/
- %p.gl-field-error.hide This field is required.
- %label.label-light Value
- %input.form-control{:name => "schedule[variables_attributes][#{i}][value]", :type => "text"}/
- %p.gl-field-error.hide This field is required.
- - else
- - (0..0).each do |i|
- .form-group
- .col-md-9
- %label.label-light Key
- %input.form-control{:name => "schedule[variables_attributes][#{i}][key]", :type => "text"}/
- %p.gl-field-error.hide This field is required.
- %label.label-light Value
- %input.form-control{:name => "schedule[variables_attributes][#{i}][value]", :type => "text"}/
- %p.gl-field-error.hide This field is required.
+ .form-group
+ .col-md-9
+ %label.label-light
+ #{ _('Variables') }
+ %ul.js-pipeline-variable-list.pipeline-variable-list
+ = f.fields_for :variables, :wrapper => false do |variable_form|
+ %li.pipeline-variable-row.fields
+ = variable_form.text_field :key, class: 'pipeline-variable-key-input form-control', placeholder: _('Input variable key')
+ = variable_form.text_area :value, class: 'pipeline-variable-value-input form-control', rows: 1, placeholder: _('Input variable value')
+ = variable_form.link_to_remove class: 'pipeline-variable-row-remove-button' do
+ %i.fa.fa-minus-circle{ aria: { hidden: "true" } }
+ = f.link_to_add :variables, 'data-target': '.js-pipeline-variable-list', class: 'pipeline-variable-row-add-button' do
+ %i.fa.fa-plus-circle{ aria: { hidden: "true" } }
+ = f.link_to_add :variables, class: '', 'data-target': '.js-pipeline-variable-list' do
+ Add variable
+ %i.fa.fa-plus-circle{ aria: { hidden: "true" } }
+
.form-group
.col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
diff --git a/vendor/assets/javascripts/jquery_nested_form.js b/vendor/assets/javascripts/jquery_nested_form.js
new file mode 100644
index 00000000000..d62a03758f5
--- /dev/null
+++ b/vendor/assets/javascripts/jquery_nested_form.js
@@ -0,0 +1,120 @@
+(function($) {
+ window.NestedFormEvents = function() {
+ this.addFields = $.proxy(this.addFields, this);
+ this.removeFields = $.proxy(this.removeFields, this);
+ };
+
+ NestedFormEvents.prototype = {
+ addFields: function(e) {
+ // Setup
+ var link = e.currentTarget;
+ var assoc = $(link).data('association'); // Name of child
+ var blueprint = $('#' + $(link).data('blueprint-id'));
+ var content = blueprint.data('blueprint'); // Fields template
+
+ // Make the context correct by replacing <parents> with the generated ID
+ // of each of the parent objects
+ var context = ($(link).closest('.fields').closestChild('input, textarea, select').eq(0).attr('name') || '').replace(/\[[a-z_]+\]$/, '');
+
+ // If the parent has no inputs we need to strip off the last pair
+ var current = content.match(new RegExp('\\[([a-z_]+)\\]\\[new_' + assoc + '\\]'));
+ if (current) {
+ context = context.replace(new RegExp('\\[' + current[1] + '\\]\\[(new_)?\\d+\\]$'), '');
+ }
+
+ // context will be something like this for a brand new form:
+ // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105]
+ // or for an edit form:
+ // project[tasks_attributes][0][assignments_attributes][1]
+ if (context) {
+ var parentNames = context.match(/[a-z_]+_attributes(?=\]\[(new_)?\d+\])/g) || [];
+ var parentIds = context.match(/[0-9]+/g) || [];
+
+ for(var i = 0; i < parentNames.length; i++) {
+ if(parentIds[i]) {
+ content = content.replace(
+ new RegExp('(_' + parentNames[i] + ')_.+?_', 'g'),
+ '$1_' + parentIds[i] + '_');
+
+ content = content.replace(
+ new RegExp('(\\[' + parentNames[i] + '\\])\\[.+?\\]', 'g'),
+ '$1[' + parentIds[i] + ']');
+ }
+ }
+ }
+
+ // Make a unique ID for the new child
+ var regexp = new RegExp('new_' + assoc, 'g');
+ var new_id = this.newId();
+ content = $.trim(content.replace(regexp, new_id));
+
+ var field = this.insertFields(content, assoc, link);
+ // bubble up event upto document (through form)
+ field
+ .trigger({ type: 'nested:fieldAdded', field: field })
+ .trigger({ type: 'nested:fieldAdded:' + assoc, field: field });
+ return false;
+ },
+ newId: function() {
+ return new Date().getTime();
+ },
+ insertFields: function(content, assoc, link) {
+ var target = $(link).data('target');
+ if (target) {
+ return $(content).appendTo($(target));
+ } else {
+ return $(content).insertBefore(link);
+ }
+ },
+ removeFields: function(e) {
+ var $link = $(e.currentTarget),
+ assoc = $link.data('association'); // Name of child to be removed
+
+ var hiddenField = $link.prev('input[type=hidden]');
+ hiddenField.val('1');
+
+ var field = $link.closest('.fields');
+ field.hide();
+
+ field
+ .trigger({ type: 'nested:fieldRemoved', field: field })
+ .trigger({ type: 'nested:fieldRemoved:' + assoc, field: field });
+ return false;
+ }
+ };
+
+ window.nestedFormEvents = new NestedFormEvents();
+ $(document)
+ .delegate('form a.add_nested_fields', 'click', nestedFormEvents.addFields)
+ .delegate('form a.remove_nested_fields', 'click', nestedFormEvents.removeFields);
+})(jQuery);
+
+// http://plugins.jquery.com/project/closestChild
+/*
+ * Copyright 2011, Tobias Lindig
+ *
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ */
+(function($) {
+ $.fn.closestChild = function(selector) {
+ // breadth first search for the first matched node
+ if (selector && selector != '') {
+ var queue = [];
+ queue.push(this);
+ while(queue.length > 0) {
+ var node = queue.shift();
+ var children = node.children();
+ for(var i = 0; i < children.length; ++i) {
+ var child = $(children[i]);
+ if (child.is(selector)) {
+ return child; //well, we found one
+ }
+ queue.push(child);
+ }
+ }
+ }
+ return $();//nothing found
+ };
+})(jQuery);