diff options
author | Eric Eastwood <contact@ericeastwood.com> | 2018-01-26 09:25:26 +0300 |
---|---|---|
committer | Eric Eastwood <contact@ericeastwood.com> | 2018-01-31 22:18:32 +0300 |
commit | 332aa756d20e8388fd402fc5c3acbf504f4cf09a (patch) | |
tree | 7bd45ed01e7934ba16e22673c47e73a942a5b842 /app/assets | |
parent | 03f386c2b20f95272e4846f5ecab54fd8b2566e0 (diff) |
Refactor CI variable list code for usage with CI/CD settings page secret variables
Part of https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110
Diffstat (limited to 'app/assets')
12 files changed, 343 insertions, 165 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; - } -} |