Commit f4dadfcd authored by Phil Hughes's avatar Phil Hughes

Merge branch '9439-searchable-environments' into 'master'

Creates environment search dropdown

Closes #9439

See merge request gitlab-org/gitlab-ee!9417
parents 3a085e85 4e1a2bd5
...@@ -22,6 +22,10 @@ export default { ...@@ -22,6 +22,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
environmentsEndpoint: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError']), ...mapState(['error', 'name', 'description', 'scopes', 'isLoading', 'hasError']),
...@@ -55,6 +59,7 @@ export default { ...@@ -55,6 +59,7 @@ export default {
:scopes="scopes" :scopes="scopes"
:cancel-path="path" :cancel-path="path"
:submit-text="__('Save changes')" :submit-text="__('Save changes')"
:environments-endpoint="environmentsEndpoint"
@handleSubmit="data => updateFeatureFlag(data)" @handleSubmit="data => updateFeatureFlag(data)"
/> />
</template> </template>
......
<script>
import _ from 'underscore';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import createFlash from '~/flash';
/**
* Creates a searchable input for environments.
*
* When given a value, it will render it as selected value
* Otherwise it will render a placeholder for the search
* input.
*
* When the user types, it will trigger an event to allow
* for API queries outside of the component.
*
* When results are returned, it renders a selectable
* list with the suggestions
*
* When no results are returned, it will render a
* button with a `Create` label. When clicked, it will
* emit an event to allow for the creation of a new
* record.
*
*/
export default {
name: 'EnvironmentsSearchableInput',
components: {
GlButton,
GlLoadingIcon,
Icon,
},
props: {
endpoint: {
type: String,
required: true,
},
value: {
type: String,
required: false,
default: '',
},
placeholder: {
type: String,
required: false,
default: __('Search an environment spec'),
},
createButtonLabel: {
type: String,
required: false,
default: __('Create'),
},
},
data() {
return {
filter: this.value || '',
results: [],
showSuggestions: false,
isLoading: false,
};
},
computed: {
/**
* Creates a label with the value of the filter
* @returns {String}
*/
composedCreateButtonLabel() {
return `${this.createButtonLabel} ${this.filter}`;
},
/**
* Create button is available when
* - loading is false, filter is set and no results are available
* @returns Boolean
*/
shouldRenderCreateButton() {
return !_.isEmpty(this.filter) && !this.isLoading && !this.results.length;
},
},
watch: {
value(newVal) {
this.filter = newVal;
},
},
methods: {
/**
* On each input event, it updates the filter value and fetches the
* list of environments based on the value typed.
*
* Since we need to update the input value both with the value provided by the parent
* and the value typed by the user, we can't use v-model.
*/
fetchEnvironments(evt) {
this.filter = evt.target.value;
this.isLoading = true;
this.openSuggestions();
return axios
.get(this.endpoint, { params: { query: this.filter } })
.then(({ data }) => {
this.results = data;
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
this.closeSuggestions();
createFlash(__('Something went wrong on our end. Please try again.'));
});
},
/**
* Opens the list of suggestions
*/
openSuggestions() {
this.showSuggestions = true;
},
/**
* Closes the list of suggestions and cleans the results
*/
closeSuggestions() {
this.showSuggestions = false;
this.results = [];
},
/**
* On click, it will:
* 1. clear the input value
* 2. close the list of suggestions
* 3. emit an event
*/
clearInput() {
this.filter = '';
this.closeSuggestions();
this.$emit('clearInput');
},
/**
* When the user selects a value from the list of suggestions
*
* It emits an event with the selected value
* Clears the filter
* and closes the list of suggestions
*
* @param {String} selected
*/
selectEnvironment(selected) {
this.$emit('selectEnvironment', selected);
this.filter = '';
this.closeSuggestions();
},
/**
* When the user clicks the create button
* it emits an event with the filter value
* Clears the input and closes the list of suggestions.
*/
createClicked() {
this.$emit('createClicked', this.filter);
this.filter = '';
this.closeSuggestions();
},
},
};
</script>
<template>
<div>
<div class="dropdown position-relative">
<icon name="search" class="seach-icon-input" />
<input
type="text"
class="js-env-input form-control pl-4"
:aria-label="placeholder"
:value="filter"
:placeholder="placeholder"
@input="fetchEnvironments"
/>
<gl-button
class="js-clear-search-input btn-transparent clear-search-input position-right-0"
@click="clearInput"
>
<icon name="clear" :aria-label="__('Clear input')" />
</gl-button>
<div
v-show="showSuggestions"
class="dropdown-menu d-block dropdown-menu-selectable dropdown-menu-full-width"
>
<div class="dropdown-content">
<gl-loading-icon v-if="isLoading" />
<ul v-else-if="results.length">
<li v-for="(result, i) in results" :key="i">
<gl-button class="btn-transparent" @click="selectEnvironment(result)">{{
result
}}</gl-button>
</li>
</ul>
<div v-else-if="!results.length" class="text-secondary p-2">
{{ __('No matching results') }}
</div>
<div v-if="shouldRenderCreateButton" class="dropdown-footer">
<gl-button class="js-create-button btn-blank dropdown-item" @click="createClicked">{{
composedCreateButtonLabel
}}</gl-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script> <script>
import _ from 'underscore';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import EnvironmentsDropdown from './environments_dropdown.vue';
export default { export default {
components: { components: {
GlButton, GlButton,
ToggleButton, ToggleButton,
Icon, Icon,
EnvironmentsDropdown,
}, },
props: { props: {
name: { name: {
...@@ -35,6 +36,10 @@ export default { ...@@ -35,6 +36,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
environmentsEndpoint: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -65,13 +70,6 @@ export default { ...@@ -65,13 +70,6 @@ export default {
return this.formScopes.filter(scope => !scope._destroy); return this.formScopes.filter(scope => !scope._destroy);
}, },
}, },
watch: {
newScope(newVal) {
if (!_.isEmpty(newVal)) {
this.addNewScope();
}
},
},
methods: { methods: {
isAllEnvironment(name) { isAllEnvironment(name) {
return name === this.$options.all; return name === this.$options.all;
...@@ -80,19 +78,24 @@ export default { ...@@ -80,19 +78,24 @@ export default {
* When the user updates the status of * When the user updates the status of
* an existing scope we toggle the status for * an existing scope we toggle the status for
* the `formScopes` * the `formScopes`
*
* @param {Object} scope
* @param {Number} index
* @param {Boolean} status
*/ */
onUpdateScopeStatus(scope, index, status) { onUpdateScopeStatus(scope, index, status) {
this.formScopes.splice(index, 1, Object.assign({}, scope, { active: status })); this.formScopes.splice(index, 1, Object.assign({}, scope, { active: status }));
}, },
addNewScope() { /**
const uniqueId = _.uniqueId('scope_'); * When the user selects or creates a new scope in the environemnts dropdoown
this.formScopes.push({ environment_scope: this.newScope, active: false, uniqueId }); * we update the selected value.
*
this.$nextTick(() => { * @param {String} name
this.$refs[uniqueId][0].focus(); * @param {Object} scope
* @param {Number} index
this.newScope = ''; */
}); updateScope(name, scope, index) {
this.formScopes.splice(index, 1, Object.assign({}, scope, { environment_scope: name }));
}, },
/** /**
* When the user clicks the toggle button in the new row, * When the user clicks the toggle button in the new row,
...@@ -114,6 +117,9 @@ export default { ...@@ -114,6 +117,9 @@ export default {
* If the scope has an ID, we need to add the `_destroy` flag * If the scope has an ID, we need to add the `_destroy` flag
* otherwise we can just remove it. * otherwise we can just remove it.
* Backend needs the destroy flag only in the PUT request. * Backend needs the destroy flag only in the PUT request.
*
* @param {Number} index
* @param {Object} scope
*/ */
removeScope(index, scope) { removeScope(index, scope) {
if (scope.id) { if (scope.id) {
...@@ -122,6 +128,20 @@ export default { ...@@ -122,6 +128,20 @@ export default {
this.formScopes.splice(index, 1); this.formScopes.splice(index, 1);
} }
}, },
/**
* When the user selects a value or creates a new value in the environments
* dropdown in the creation row, we push a new entry with the selected value.
*
* @param {String}
*/
createNewEnvironment(name) {
this.formScopes.push({ environment_scope: name, active: false });
this.newScope = '';
},
/**
* When the user clicks the submit button
* it triggers an event with the form data
*/
handleSubmit() { handleSubmit() {
this.$emit('handleSubmit', { this.$emit('handleSubmit', {
name: this.formName, name: this.formName,
...@@ -161,7 +181,7 @@ export default { ...@@ -161,7 +181,7 @@ export default {
<h4>{{ s__('FeatureFlags|Target environments') }}</h4> <h4>{{ s__('FeatureFlags|Target environments') }}</h4>
<div v-html="$options.helpText"></div> <div v-html="$options.helpText"></div>
<div class="js-scopes-table table-holder prepend-top-default"> <div class="js-scopes-table prepend-top-default">
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-60" role="columnheader"> <div class="table-section section-60" role="columnheader">
{{ s__('FeatureFlags|Environment Spec') }} {{ s__('FeatureFlags|Environment Spec') }}
...@@ -182,15 +202,18 @@ export default { ...@@ -182,15 +202,18 @@ export default {
{{ s__('FeatureFlags|Environment Spec') }} {{ s__('FeatureFlags|Environment Spec') }}
</div> </div>
<div class="table-mobile-content js-feature-flag-status"> <div class="table-mobile-content js-feature-flag-status">
<p v-if="isAllEnvironment(scope.environment_scope)" class="js-scope-all"> <p v-if="isAllEnvironment(scope.environment_scope)" class="js-scope-all pl-3">
{{ $options.allEnvironments }} {{ $options.allEnvironments }}
</p> </p>
<input
<environments-dropdown
v-else v-else
:ref="scope.uniqueId" class="col-md-6"
v-model="scope.environment_scope" :value="scope.environment_scope"
type="text" :endpoint="environmentsEndpoint"
class="form-control col-md-6 prepend-left-4" @selectEnvironment="env => updateScope(env, scope, index)"
@createClicked="env => updateScope(env, scope, index)"
@clearInput="updateScope('', scope, index)"
/> />
</div> </div>
</div> </div>
...@@ -229,10 +252,12 @@ export default { ...@@ -229,10 +252,12 @@ export default {
{{ s__('FeatureFlags|Environment Spec') }} {{ s__('FeatureFlags|Environment Spec') }}
</div> </div>
<div class="table-mobile-content js-feature-flag-status"> <div class="table-mobile-content js-feature-flag-status">
<input <environments-dropdown
v-model="newScope" class="js-new-scope-name col-md-6"
type="text" :endpoint="environmentsEndpoint"
class="js-new-scope-name form-control col-md-6 prepend-left-4" :value="newScope"
@selectEnvironment="env => createNewEnvironment(env)"
@createClicked="env => createNewEnvironment(env)"
/> />
</div> </div>
</div> </div>
......
...@@ -19,6 +19,10 @@ export default { ...@@ -19,6 +19,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
environmentsEndpoint: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState(['error']), ...mapState(['error']),
...@@ -52,6 +56,7 @@ export default { ...@@ -52,6 +56,7 @@ export default {
:cancel-path="path" :cancel-path="path"
:submit-text="s__('FeatureFlags|Create feature flag')" :submit-text="s__('FeatureFlags|Create feature flag')"
:scopes="scopes" :scopes="scopes"
:environments-endpoint="environmentsEndpoint"
@handleSubmit="data => createFeatureFlag(data)" @handleSubmit="data => createFeatureFlag(data)"
/> />
</div> </div>
......
...@@ -14,6 +14,7 @@ export default () => { ...@@ -14,6 +14,7 @@ export default () => {
props: { props: {
endpoint: el.dataset.endpoint, endpoint: el.dataset.endpoint,
path: el.dataset.featureFlagsPath, path: el.dataset.featureFlagsPath,
environmentsEndpoint: el.dataset.environmentsEndpoint,
}, },
}); });
}, },
......
...@@ -14,6 +14,7 @@ export default () => { ...@@ -14,6 +14,7 @@ export default () => {
props: { props: {
endpoint: el.dataset.endpoint, endpoint: el.dataset.endpoint,
path: el.dataset.featureFlagsPath, path: el.dataset.featureFlagsPath,
environmentsEndpoint: el.dataset.environmentsEndpoint,
}, },
}); });
}, },
......
...@@ -3,11 +3,6 @@ export const parseFeatureFlagsParams = params => ({ ...@@ -3,11 +3,6 @@ export const parseFeatureFlagsParams = params => ({
operations_feature_flag: { operations_feature_flag: {
name: params.name, name: params.name,
description: params.description, description: params.description,
// removes uniqueId key used in creation form scopes_attributes: params.scopes,
scopes_attributes: params.scopes.map(scope => {
const scopeCopy = Object.assign({}, scope);
delete scopeCopy.uniqueId;
return scopeCopy;
}),
}, },
}); });
...@@ -9,3 +9,19 @@ $label-blue: #428bca; ...@@ -9,3 +9,19 @@ $label-blue: #428bca;
color: $white-light; color: $white-light;
background-color: $gray-500; background-color: $gray-500;
} }
.seach-icon-input,
.clear-search-input {
position: absolute;
z-index: 10;
fill: $gl-gray-400;
}
.seach-icon-input {
left: 4px;
top: 10px;
}
.clear-search-input {
top: 1px;
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- page_title s_('FeatureFlags|Edit Feature Flag') - page_title s_('FeatureFlags|Edit Feature Flag')
- if Feature.enabled?(:feature_flags_environment_scope, @project) - if Feature.enabled?(:feature_flags_environment_scope, @project)
#js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag), feature_flags_path: project_feature_flags_path(@project) } } #js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag), feature_flags_path: project_feature_flags_path(@project), environments_endpoint: search_project_environments_path(@project, format: :json)} }
- else - else
%h3.page-title %h3.page-title
= s_('FeatureFlags|Edit %{feature_flag_name}') % { feature_flag_name: @feature_flag.name } = s_('FeatureFlags|Edit %{feature_flag_name}') % { feature_flag_name: @feature_flag.name }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- page_title s_('FeatureFlags|New Feature Flag') - page_title s_('FeatureFlags|New Feature Flag')
- if Feature.enabled?(:feature_flags_environment_scope, @project) - if Feature.enabled?(:feature_flags_environment_scope, @project)
#js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json), feature_flags_path: project_feature_flags_path(@project) } } #js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json), feature_flags_path: project_feature_flags_path(@project), environments_endpoint: search_project_environments_path(@project, format: :json) } }
- else - else
%h3.page-title %h3.page-title
= s_('FeatureFlags|New Feature Flag') = s_('FeatureFlags|New Feature Flag')
......
...@@ -65,7 +65,10 @@ describe 'User creates feature flag', :js do ...@@ -65,7 +65,10 @@ describe 'User creates feature flag', :js do
set_feature_flag_info('mr_train', '') set_feature_flag_info('mr_train', '')
within_scope_row(2) do within_scope_row(2) do
within_environment_spec { find('.js-new-scope-name').set("review/*") } within_environment_spec do
find('.js-env-input').set("review/*")
find('.js-create-button').click
end
end end
within_scope_row(2) do within_scope_row(2) do
...@@ -90,6 +93,38 @@ describe 'User creates feature flag', :js do ...@@ -90,6 +93,38 @@ describe 'User creates feature flag', :js do
end end
end end
context 'when searches an environment name for scope creation' do
let!(:environment) { create(:environment, name: 'production', project: project) }
before do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('mr_train', '')
within_scope_row(2) do
within_environment_spec do
find('.js-env-input').set('prod')
click_button 'production'
end
end
click_button 'Create feature flag'
end
it 'shows the created feature flag' do
within_feature_flag_row(1) do
expect(page.find('.feature-flag-name')).to have_content('mr_train')
expect(page).to have_css('.js-feature-flag-status .badge-success')
within_feature_flag_scopes do
expect(page.find('.badge:nth-child(1)')).to have_content('*')
expect(page.find('.badge:nth-child(1)')['class']).to include('badge-active')
expect(page.find('.badge:nth-child(2)')).to have_content('production')
expect(page.find('.badge:nth-child(2)')['class']).to include('badge-inactive')
end
end
end
end
private private
def set_feature_flag_info(name, description) def set_feature_flag_info(name, description)
......
...@@ -63,7 +63,10 @@ describe 'User updates feature flag', :js do ...@@ -63,7 +63,10 @@ describe 'User updates feature flag', :js do
context 'when user adds a new scope' do context 'when user adds a new scope' do
before do before do
within_scope_row(3) do within_scope_row(3) do
within_environment_spec { find('.js-new-scope-name').set('production') } within_environment_spec do
find('.js-env-input').set('production')
find('.js-create-button').click
end
end end
click_button 'Save changes' click_button 'Save changes'
......
...@@ -26,6 +26,7 @@ describe('Edit feature flag form', () => { ...@@ -26,6 +26,7 @@ describe('Edit feature flag form', () => {
propsData: { propsData: {
endpoint: `${TEST_HOST}/feature_flags.json'`, endpoint: `${TEST_HOST}/feature_flags.json'`,
path: '/feature_flags', path: '/feature_flags',
environmentsEndpoint: 'environments.json',
}, },
store, store,
sync: false, sync: false,
......
import MockAdapter from 'axios-mock-adapter';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import EnvironmentsDropdown from 'ee/feature_flags/components/environments_dropdown.vue';
import { TEST_HOST } from 'spec/test_constants';
const localVue = createLocalVue();
describe('Feature Flags > Environments dropdown ', () => {
let wrapper;
let mock;
const factory = props => {
wrapper = mount(localVue.extend(EnvironmentsDropdown), {
localVue,
propsData: {
endpoint: `${TEST_HOST}/environments.json'`,
...props,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
mock.restore();
});
beforeEach(() => {
mock = new MockAdapter(axios);
});
describe('without value', () => {
it('renders the placeholder', () => {
factory();
expect(wrapper.find('input').attributes('placeholder')).toEqual('Search an environment spec');
});
});
describe('with value', () => {
it('sets filter to equal the value', () => {
factory({ value: 'production' });
expect(wrapper.vm.filter).toEqual('production');
});
});
describe('on input change', () => {
const results = ['production', 'staging'];
describe('on success', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(200, results);
factory();
wrapper.find('input').setValue('production');
});
it('sets filter value', () => {
expect(wrapper.vm.filter).toEqual('production');
});
describe('with received data', () => {
it('sets is loading to false', done => {
setTimeout(() => {
expect(wrapper.vm.isLoading).toEqual(false);
expect(wrapper.find(GlLoadingIcon).exists()).toEqual(false);
done();
});
});
it('sets results with the received data', done => {
setTimeout(() => {
expect(wrapper.vm.results).toEqual(results);
done();
});
});
it('sets showSuggestions to true', done => {
setTimeout(() => {
expect(wrapper.vm.showSuggestions).toEqual(true);
done();
});
});
it('emits even when a suggestion is clicked', done => {
setTimeout(() => {
spyOn(wrapper.vm, '$emit');
wrapper.find('ul button').trigger('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('selectEnvironment', 'production');
done();
});
});
});
});
});
describe('on click clear button', () => {
beforeEach(() => {
wrapper.find('.js-clear-search-input').trigger('click');
});
it('resets filter value', () => {
expect(wrapper.vm.filter).toEqual('');
});
it('closes list of suggestions', () => {
expect(wrapper.vm.showSuggestions).toEqual(false);
});
});
describe('on click create button', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(200, []);
factory();
wrapper.find('input').setValue('production');
});
it('emits create event', done => {
setTimeout(() => {
spyOn(wrapper.vm, '$emit');
wrapper.find('.js-create-button').trigger('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('createClicked', 'production');
done();
});
});
});
});
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Form from 'ee/feature_flags/components/form.vue'; import Form from 'ee/feature_flags/components/form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import EnvironmentsDropdown from 'ee/feature_flags/components/environments_dropdown.vue';
describe('feature flag form', () => { describe('feature flag form', () => {
let wrapper; let wrapper;
const requiredProps = { const requiredProps = {
cancelPath: 'feature_flags', cancelPath: 'feature_flags',
submitText: 'Create', submitText: 'Create',
environmentsEndpoint: '/environments.json',
}; };
const factory = (props = {}) => { const factory = (props = {}) => {
...@@ -58,20 +60,6 @@ describe('feature flag form', () => { ...@@ -58,20 +60,6 @@ describe('feature flag form', () => {
}); });
describe('status toggle', () => { describe('status toggle', () => {
describe('with filled text input', () => {
it('should add a new scope with the text value and the status and reset the form', () => {
wrapper.find('.js-new-scope-name').setValue('production');
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(wrapper.vm.formScopes).toEqual([
{ active: true, environment_scope: 'production' },
]);
expect(wrapper.vm.newScope).toEqual('');
});
});
describe('without filled text input', () => { describe('without filled text input', () => {
it('should add a new scope with the text value empty and the status', () => { it('should add a new scope with the text value empty and the status', () => {
wrapper.find(ToggleButton).vm.$emit('change', true); wrapper.find(ToggleButton).vm.$emit('change', true);
...@@ -203,7 +191,11 @@ describe('feature flag form', () => { ...@@ -203,7 +191,11 @@ describe('feature flag form', () => {
it('should emit handleSubmit with the updated data', () => { it('should emit handleSubmit with the updated data', () => {
wrapper.find('#feature-flag-name').setValue('feature_flag_2'); wrapper.find('#feature-flag-name').setValue('feature_flag_2');
wrapper.find('.js-new-scope-name').setValue('review'); wrapper
.find('.js-new-scope-name')
.find(EnvironmentsDropdown)
.vm.$emit('selectEnvironment', 'review');
wrapper wrapper
.find('.js-add-new-scope') .find('.js-add-new-scope')
.find(ToggleButton) .find(ToggleButton)
...@@ -222,6 +214,10 @@ describe('feature flag form', () => { ...@@ -222,6 +214,10 @@ describe('feature flag form', () => {
}, },
{ {
environment_scope: 'review', environment_scope: 'review',
active: false,
},
{
environment_scope: '',
active: true, active: true,
}, },
], ],
......
...@@ -23,6 +23,7 @@ describe('New feature flag form', () => { ...@@ -23,6 +23,7 @@ describe('New feature flag form', () => {
propsData: { propsData: {
endpoint: 'feature_flags.json', endpoint: 'feature_flags.json',
path: '/feature_flags', path: '/feature_flags',
environmentsEndpoint: 'environments.json',
}, },
store, store,
sync: false, sync: false,
......
...@@ -1883,6 +1883,9 @@ msgstr "" ...@@ -1883,6 +1883,9 @@ msgstr ""
msgid "Clear" msgid "Clear"
msgstr "" msgstr ""
msgid "Clear input"
msgstr ""
msgid "Clear search" msgid "Clear search"
msgstr "" msgstr ""
...@@ -6333,6 +6336,9 @@ msgstr "" ...@@ -6333,6 +6336,9 @@ msgstr ""
msgid "No license. All rights reserved" msgid "No license. All rights reserved"
msgstr "" msgstr ""
msgid "No matching results"
msgstr ""
msgid "No merge requests for the selected time period." msgid "No merge requests for the selected time period."
msgstr "" msgstr ""
...@@ -8151,6 +8157,9 @@ msgstr "" ...@@ -8151,6 +8157,9 @@ msgstr ""
msgid "Search" msgid "Search"
msgstr "" msgstr ""
msgid "Search an environment spec"
msgstr ""
msgid "Search branches" msgid "Search branches"
msgstr "" msgstr ""
...@@ -8639,6 +8648,9 @@ msgstr "" ...@@ -8639,6 +8648,9 @@ msgstr ""
msgid "Something went wrong on our end. Please try again!" msgid "Something went wrong on our end. Please try again!"
msgstr "" msgstr ""
msgid "Something went wrong on our end. Please try again."
msgstr ""
msgid "Something went wrong trying to change the confidentiality of this issue" msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment