diff options
author | Matija Čupić <matteeyah@gmail.com> | 2018-02-03 00:27:59 +0100 |
---|---|---|
committer | Matija Čupić <matteeyah@gmail.com> | 2018-02-03 00:27:59 +0100 |
commit | 88c9199abd83a1402c8699d0f9ee63ba5b41c18f (patch) | |
tree | 51805b66ebc9a945b98fb36de29502e402b2648b /app | |
parent | 20714ee90232b5577fc99f2a773ddccf71482c08 (diff) | |
parent | a3166376bb50d84540ba2c60f5f5131ee211ffa8 (diff) | |
download | gitlab-ce-88c9199abd83a1402c8699d0f9ee63ba5b41c18f.tar.gz |
Merge branch 'master' into persistent-calloutspersistent-callouts
Diffstat (limited to 'app')
15 files changed, 403 insertions, 187 deletions
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 7f70fce913a..0d6e0dbefcc 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -15,10 +15,12 @@ export default class SecretValues { init() { this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); - const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); - this.updateDom(isRevealed); + if (this.revealButton) { + const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); + this.updateDom(isRevealed); - this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + } } onRevealButtonClicked() { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js new file mode 100644 index 00000000000..e46478ddb98 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -0,0 +1,205 @@ +import $ from 'jquery'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; +import CreateItemDropdown from '../create_item_dropdown'; +import SecretValues from '../behaviors/secret_values'; + +const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments'); + +function createEnvironmentItem(value) { + return { + title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, + id: value, + text: value, + }; +} + +export default class VariableList { + constructor({ + container, + formField, + }) { + this.$container = $(container); + this.formField = formField; + this.environmentDropdownMap = new WeakMap(); + + this.inputMap = { + id: { + selector: '.js-ci-variable-input-id', + default: '', + }, + key: { + selector: '.js-ci-variable-input-key', + default: '', + }, + value: { + selector: '.js-ci-variable-input-value', + default: '', + }, + protected: { + selector: '.js-ci-variable-input-protected', + default: 'true', + }, + environment: { + // We can't use a `.js-` class here because + // gl_dropdown replaces the <input> and doesn't copy over the class + // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 + selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, + default: '*', + }, + _destroy: { + selector: '.js-ci-variable-input-destroy', + default: '', + }, + }; + + this.secretValues = new SecretValues({ + container: this.$container[0], + valueSelector: '.js-row:not(:last-child) .js-secret-value', + placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder', + }); + } + + init() { + this.bindEvents(); + this.secretValues.init(); + } + + bindEvents() { + this.$container.find('.js-row').each((index, rowEl) => { + this.initRow(rowEl); + }); + + this.$container.on('click', '.js-row-remove-button', (e) => { + e.preventDefault(); + this.removeRow($(e.currentTarget).closest('.js-row')); + }); + + const inputSelector = Object.keys(this.inputMap) + .map(name => this.inputMap[name].selector) + .join(','); + + // Remove any empty rows except the last row + this.$container.on('blur', inputSelector, (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { + this.removeRow($row); + } + }); + + // Always make sure there is an empty last row + this.$container.on('input trigger-change', inputSelector, () => { + const $lastRow = this.$container.find('.js-row').last(); + + if (this.checkIfRowTouched($lastRow)) { + this.insertRow($lastRow); + } + }); + } + + initRow(rowEl) { + const $row = $(rowEl); + + setupToggleButtons($row[0]); + + const $environmentSelect = $row.find('.js-variable-environment-toggle'); + if ($environmentSelect.length) { + const createItemDropdown = new CreateItemDropdown({ + $dropdown: $environmentSelect, + defaultToggleLabel: ALL_ENVIRONMENTS_STRING, + fieldName: `${this.formField}[variables_attributes][][environment]`, + getData: (term, callback) => callback(this.getEnvironmentValues()), + createNewItemFromValue: createEnvironmentItem, + onSelect: () => { + // Refresh the other dropdowns in the variable list + // so they have the new value we just picked + this.refreshDropdownData(); + + $row.find(this.inputMap.environment.selector).trigger('trigger-change'); + }, + }); + + // Clear out any data that might have been left-over from the row clone + createItemDropdown.clearDropdown(); + + this.environmentDropdownMap.set($row[0], createItemDropdown); + } + } + + insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + + // Reset the inputs to their defaults + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + $rowClone.find(entry.selector).val(entry.default); + }); + + this.initRow($rowClone); + + $row.after($rowClone); + } + + removeRow($row) { + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + // eslint-disable-next-line no-underscore-dangle + .find(this.inputMap._destroy.selector) + .val(true); + } else { + $row.remove(); + } + } + + checkIfRowTouched($row) { + return Object.keys(this.inputMap).some((name) => { + const entry = this.inputMap[name]; + const $el = $row.find(entry.selector); + return $el.length && $el.val() !== entry.default; + }); + } + + getAllData() { + // Ignore the last empty row because we don't want to try persist + // a blank variable and run into validation problems. + const validRows = this.$container.find('.js-row').toArray().slice(0, -1); + + return validRows.map((rowEl) => { + const resultant = {}; + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + const $input = $(rowEl).find(entry.selector); + if ($input.length) { + resultant[name] = $input.val(); + } + }); + + return resultant; + }); + } + + getEnvironmentValues() { + const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() + .reduce((prevValueMap, envInput) => ({ + ...prevValueMap, + [envInput.value]: envInput.value, + }), {}); + + return Object.keys(valueMap).map(createEnvironmentItem); + } + + refreshDropdownData() { + this.$container.find('.js-row').each((index, rowEl) => { + const environmentDropdown = this.environmentDropdownMap.get(rowEl); + if (environmentDropdown) { + environmentDropdown.refreshData(); + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js new file mode 100644 index 00000000000..d54ea7df1c3 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -0,0 +1,26 @@ +import VariableList from './ci_variable_list'; + +// Used for the variable list on scheduled pipeline edit page +export default function setupNativeFormVariableList({ + container, + formField = 'variables', +}) { + const $container = $(container); + + const variableList = new VariableList({ + container: $container, + formField, + }); + variableList.init(); + + // Clear out the names in the empty last row so it + // doesn't get submitted and throw validation errors + $container.closest('form').on('submit trigger-submit', () => { + const $lastRow = $container.find('.js-row').last(); + + const isTouched = variableList.checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index ff9e4485916..46232726510 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,8 @@ import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; import 'core-js/fn/symbol'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; // Browser polyfills import 'classlist-polyfill'; 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 f1cf6e92ef5..0b1a81bae13 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -4,7 +4,7 @@ import GlFieldErrors from '../gl_field_errors'; 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'; +import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list'; Vue.use(Translate); @@ -42,5 +42,8 @@ document.addEventListener('DOMContentLoaded', () => { gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement); - setupPipelineVariableList($('.js-pipeline-variable-list')); + setupNativeFormVariableList({ + container: $('.js-ci-variable-list-section'), + formField: 'schedule', + }); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js deleted file mode 100644 index 9e0e5cacb11..00000000000 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ /dev/null @@ -1,73 +0,0 @@ -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; - -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 = 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/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js index 974dc3ee052..2d680d0f0dc 100644 --- a/app/assets/javascripts/toggle_buttons.js +++ b/app/assets/javascripts/toggle_buttons.js @@ -13,7 +13,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils'; ``` */ -function updatetoggle(toggle, isOn) { +function updateToggle(toggle, isOn) { toggle.classList.toggle('is-checked', isOn); } @@ -21,7 +21,7 @@ function onToggleClicked(toggle, input, clickCallback) { const previousIsOn = convertPermissionToBoolean(input.value); // Visually change the toggle and start loading - updatetoggle(toggle, !previousIsOn); + updateToggle(toggle, !previousIsOn); toggle.setAttribute('disabled', true); toggle.classList.toggle('is-loading', true); @@ -32,7 +32,7 @@ function onToggleClicked(toggle, input, clickCallback) { }) .catch(() => { // Revert the visuals if something goes wrong - updatetoggle(toggle, previousIsOn); + updateToggle(toggle, previousIsOn); }) .then(() => { // Remove the loading indicator in any case @@ -54,7 +54,7 @@ export default function setupToggleButtons(container, clickCallback = () => {}) const isOn = convertPermissionToBoolean(input.value); // Get the visible toggle in sync with the hidden input - updatetoggle(toggle, isOn); + updateToggle(toggle, isOn); toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); }); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index cff47ea76ec..c4aad24e9c1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -60,3 +60,4 @@ @import "framework/memory_graph"; @import "framework/responsive_tables"; @import "framework/stacked-progress-bar"; +@import "framework/ci_variable_list"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d0b0c69b18f..c4b046a6d68 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -176,6 +176,11 @@ &.btn-remove { @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } + + &.btn-primary, + &.btn-info { + @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); + } } &.btn-gray { diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss new file mode 100644 index 00000000000..8f654ab363c --- /dev/null +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -0,0 +1,88 @@ +.ci-variable-list { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; + clear: both; +} + +.ci-variable-row { + display: flex; + align-items: flex-end; + + &:not(:last-child) { + margin-bottom: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-bottom: 3 * $gl-btn-padding; + } + } + + &:last-child { + .ci-variable-body-item:last-child { + margin-right: $ci-variable-remove-button-width; + + @media (max-width: $screen-xs-max) { + margin-right: 0; + } + } + + .ci-variable-row-remove-button { + display: none; + } + + @media (max-width: $screen-xs-max) { + .ci-variable-row-body { + margin-right: $ci-variable-remove-button-width; + } + } + } +} + +.ci-variable-row-body { + display: flex; + width: 100%; + + @media (max-width: $screen-xs-max) { + display: block; + } +} + +.ci-variable-body-item { + flex: 1; + + &:not(:last-child) { + margin-right: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-right: 0; + margin-bottom: $gl-btn-padding; + } + } +} + +.ci-variable-protected-item { + flex: 0 1 auto; + display: flex; + align-items: center; +} + +.ci-variable-row-remove-button { + @include transition(color); + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $ci-variable-remove-button-width; + height: $input-height; + padding: 0; + background: transparent; + border: 0; + color: $gl-text-color-secondary; + + &:hover, + &:focus { + outline: none; + color: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f76c6866463..1cc22f5658d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -668,9 +668,9 @@ $pipeline-dropdown-line-height: 20px; $pipeline-dropdown-status-icon-size: 18px; /* -Pipeline Schedules +CI variable lists */ -$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); +$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); /* diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index b698a4f9afa..bc7fa8a26d9 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -78,84 +78,3 @@ margin-right: 3px; } } - -.pipeline-variable-list { - margin-left: 0; - margin-bottom: 0; - padding-left: 0; - list-style: none; - clear: both; -} - -.pipeline-variable-row { - display: flex; - align-items: flex-end; - - &:not(:last-child) { - margin-bottom: $gl-btn-padding; - } - - @media (max-width: $screen-sm-max) { - padding-right: $gl-col-padding; - } - - &:last-child { - .pipeline-variable-row-remove-button { - display: none; - } - - @media (max-width: $screen-sm-max) { - .pipeline-variable-value-input { - margin-right: $pipeline-variable-remove-button-width; - } - } - - @media (max-width: $screen-xs-max) { - .pipeline-variable-row-body { - margin-right: $pipeline-variable-remove-button-width; - } - } - } -} - -.pipeline-variable-row-body { - display: flex; - width: calc(75% - #{$gl-col-padding}); - padding-left: $gl-col-padding; - - @media (max-width: $screen-sm-max) { - width: 100%; - } - - @media (max-width: $screen-xs-max) { - display: block; - } -} - -.pipeline-variable-key-input { - margin-right: $gl-btn-padding; - - @media (max-width: $screen-xs-max) { - margin-bottom: $gl-btn-padding; - } -} - -.pipeline-variable-row-remove-button { - @include transition(color); - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: center; - width: $pipeline-variable-remove-button-width; - height: $input-height; - padding: 0; - background: transparent; - border: 0; - color: $gl-text-color-secondary; - - &:hover, - &:focus { - outline: none; - color: $gl-text-color; - } -} diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml new file mode 100644 index 00000000000..495a55660cb --- /dev/null +++ b/app/views/ci/variables/_variable_row.html.haml @@ -0,0 +1,49 @@ +- form_field = local_assigns.fetch(:form_field, nil) +- variable = local_assigns.fetch(:variable, nil) +- only_key_value = local_assigns.fetch(:only_key_value, false) + +- id = variable&.id +- key = variable&.key +- value = variable&.value +- is_protected = variable && !only_key_value ? variable.protected : true + +- id_input_name = "#{form_field}[variables_attributes][][id]" +- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]" +- key_input_name = "#{form_field}[variables_attributes][][key]" +- value_input_name = "#{form_field}[variables_attributes][][value]" +- protected_input_name = "#{form_field}[variables_attributes][][protected]" + +%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } } + .ci-variable-row-body + %input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id } + %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name } + %input.js-ci-variable-input-key.ci-variable-body-item.form-control{ type: "text", + name: key_input_name, + value: key, + placeholder: s_('CiVariables|Input variable key') } + .ci-variable-body-item + .form-control.js-secret-value-placeholder{ class: ('hide' unless id) } + = '*' * 20 + %textarea.js-ci-variable-input-value.js-secret-value.form-control{ class: ('hide' if id), + rows: 1, + name: value_input_name, + placeholder: s_('CiVariables|Input variable value') } + = value + - unless only_key_value + .ci-variable-body-item.ci-variable-protected-item + .append-right-default + = s_("CiVariable|Protected") + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_protected}", + "aria-label": s_("CiVariable|Toggle protected") } + %input{ type: "hidden", + class: 'js-ci-variable-input-protected js-project-feature-toggle-input', + name: protected_input_name, + value: is_protected } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + -# EE-specific start + -# EE-specific end + %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } + = icon('minus-circle') diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 857ae00d0ab..ff440e99042 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -22,14 +22,20 @@ = 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 - .form-group + .form-group.js-ci-variable-list-section .col-md-9 %label.label-light #{ s_('PipelineSchedules|Variables') } - %ul.js-pipeline-variable-list.pipeline-variable-list - - @schedule.variables.each do |variable| - = render 'variable_row', id: variable.id, key: variable.key, value: variable.value - = render 'variable_row' + %ul.ci-variable-list + - @schedule.variables.each do |variable| + = render 'ci/variables/variable_row', form_field: 'schedule', variable: variable, only_key_value: true + = render 'ci/variables/variable_row', form_field: 'schedule', only_key_value: true + - if @schedule.variables.size > 0 + %button.btn.btn-info.btn-inverted.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } } + - if @schedule.variables.size == 0 + = n_('Hide value', 'Hide values', @schedule.variables.size) + - else + = n_('Reveal value', 'Reveal values', @schedule.variables.size) .form-group .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml deleted file mode 100644 index 564cb5d1ca9..00000000000 --- a/app/views/projects/pipeline_schedules/_variable_row.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- 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?}" } } - .pipeline-variable-row-body - %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: s_('PipelineSchedules|Input variable key') } - %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1, - name: "schedule[variables_attributes][][value]", - placeholder: s_('PipelineSchedules|Input variable value') } - = value - %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') } - %i.fa.fa-minus-circle{ 'aria-hidden': "true" } |