Commit 9e1da9fc authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Phil Hughes

Move User Lists to its own Page

parent d0429644
<script>
import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab } from '@gitlab/ui';
import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
export default {
components: { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab },
components: { GlAlert, GlEmptyState, GlLink, GlLoadingIcon },
inject: ['errorStateSvgPath', 'featureFlagsHelpPagePath'],
props: {
title: {
required: true,
type: String,
},
count: {
required: false,
type: Number,
......@@ -56,18 +52,11 @@ export default {
clearAlert(index) {
this.$emit('dismissAlert', index);
},
onClick(event) {
return this.$emit('changeTab', event);
},
},
};
</script>
<template>
<gl-tab @click="onClick">
<template #title>
<span data-testid="feature-flags-tab-title">{{ title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemCount }}</gl-badge>
</template>
<div>
<gl-alert
v-for="(message, index) in alerts"
:key="index"
......@@ -83,7 +72,7 @@ export default {
<gl-empty-state
v-else-if="errorState"
:title="errorTitle"
:description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
:description="s__('FeatureFlags|Try again in a few moments or contact your support team.')"
:svg-path="errorStateSvgPath"
data-testid="error-state"
/>
......@@ -101,6 +90,6 @@ export default {
</gl-link>
</template>
</gl-empty-state>
<slot> </slot>
</gl-tab>
<slot v-else> </slot>
</div>
</template>
......@@ -21,9 +21,6 @@ export const fetchUserIdParams = property(['parameters', 'userIds']);
export const NEW_VERSION_FLAG = 'new_version_flag';
export const LEGACY_FLAG = 'legacy_flag';
export const FEATURE_FLAG_SCOPE = 'featureFlags';
export const USER_LIST_SCOPE = 'userLists';
export const EMPTY_PARAMETERS = { parameters: {}, userListId: undefined };
export const STRATEGY_SELECTIONS = [
......
......@@ -22,7 +22,7 @@ export default () => {
unleashApiUrl,
canUserAdminFeatureFlag,
newFeatureFlagPath,
newUserListPath,
userListPath,
featureFlagsLimitExceeded,
featureFlagsLimit,
} = el.dataset;
......@@ -40,9 +40,9 @@ export default () => {
csrfToken: csrf.token,
canUserConfigure: canUserAdminFeatureFlag !== undefined,
newFeatureFlagPath,
newUserListPath,
featureFlagsLimitExceeded: featureFlagsLimitExceeded !== undefined,
featureFlagsLimit,
userListPath,
},
render(createElement) {
return createElement(FeatureFlagsComponent);
......
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
......@@ -26,19 +25,6 @@ export const receiveFeatureFlagsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response);
export const receiveFeatureFlagsError = ({ commit }) => commit(types.RECEIVE_FEATURE_FLAGS_ERROR);
export const fetchUserLists = ({ state, dispatch }) => {
dispatch('requestUserLists');
return Api.fetchFeatureFlagUserLists(state.projectId, state.options.page)
.then(({ data, headers }) => dispatch('receiveUserListsSuccess', { data, headers }))
.catch(() => dispatch('receiveUserListsError'));
};
export const requestUserLists = ({ commit }) => commit(types.REQUEST_USER_LISTS);
export const receiveUserListsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_USER_LISTS_SUCCESS, response);
export const receiveUserListsError = ({ commit }) => commit(types.RECEIVE_USER_LISTS_ERROR);
export const toggleFeatureFlag = ({ dispatch }, flag) => {
dispatch('updateFeatureFlag', flag);
......@@ -57,26 +43,6 @@ export const receiveUpdateFeatureFlagSuccess = ({ commit }, data) =>
export const receiveUpdateFeatureFlagError = ({ commit }, id) =>
commit(types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, id);
export const deleteUserList = ({ state, dispatch }, list) => {
dispatch('requestDeleteUserList', list);
return Api.deleteFeatureFlagUserList(state.projectId, list.iid)
.then(() => dispatch('fetchUserLists'))
.catch((error) =>
dispatch('receiveDeleteUserListError', {
list,
error: error?.response?.data ?? error,
}),
);
};
export const requestDeleteUserList = ({ commit }, list) =>
commit(types.REQUEST_DELETE_USER_LIST, list);
export const receiveDeleteUserListError = ({ commit }, { error, list }) => {
commit(types.RECEIVE_DELETE_USER_LIST_ERROR, { error, list });
};
export const rotateInstanceId = ({ state, dispatch }) => {
dispatch('requestRotateInstanceId');
......
......@@ -4,13 +4,6 @@ export const REQUEST_FEATURE_FLAGS = 'REQUEST_FEATURE_FLAGS';
export const RECEIVE_FEATURE_FLAGS_SUCCESS = 'RECEIVE_FEATURE_FLAGS_SUCCESS';
export const RECEIVE_FEATURE_FLAGS_ERROR = 'RECEIVE_FEATURE_FLAGS_ERROR';
export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS';
export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS';
export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR';
export const REQUEST_DELETE_USER_LIST = 'REQUEST_DELETE_USER_LIST';
export const RECEIVE_DELETE_USER_LIST_ERROR = 'RECEIVE_DELETE_USER_LIST_ERROR';
export const UPDATE_FEATURE_FLAG = 'UPDATE_FEATURE_FLAG';
export const RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS = 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS';
export const RECEIVE_UPDATE_FEATURE_FLAG_ERROR = 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR';
......
import Vue from 'vue';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../constants';
import { mapToScopesViewModel } from '../helpers';
import * as types from './mutation_types';
const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) });
const updateFlag = (state, flag) => {
const index = state[FEATURE_FLAG_SCOPE].findIndex(({ id }) => id === flag.id);
Vue.set(state[FEATURE_FLAG_SCOPE], index, flag);
const index = state.featureFlags.findIndex(({ id }) => id === flag.id);
Vue.set(state.featureFlags, index, flag);
};
const createPaginationInfo = (state, headers) => {
const createPaginationInfo = (headers) => {
let paginationInfo;
if (Object.keys(headers).length) {
const normalizedHeaders = normalizeHeaders(headers);
......@@ -32,44 +31,16 @@ export default {
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
state[FEATURE_FLAG_SCOPE] = (response.data.feature_flags || []).map(mapFlag);
state.featureFlags = (response.data.feature_flags || []).map(mapFlag);
const paginationInfo = createPaginationInfo(state, response.headers);
state.count = {
...state.count,
[FEATURE_FLAG_SCOPE]: paginationInfo?.total ?? state[FEATURE_FLAG_SCOPE].length,
};
state.pageInfo = {
...state.pageInfo,
[FEATURE_FLAG_SCOPE]: paginationInfo,
};
const paginationInfo = createPaginationInfo(response.headers);
state.count = paginationInfo?.total ?? state.featureFlags.length;
state.pageInfo = paginationInfo;
},
[types.RECEIVE_FEATURE_FLAGS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.REQUEST_USER_LISTS](state) {
state.isLoading = true;
},
[types.RECEIVE_USER_LISTS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
state[USER_LIST_SCOPE] = response.data || [];
const paginationInfo = createPaginationInfo(state, response.headers);
state.count = {
...state.count,
[USER_LIST_SCOPE]: paginationInfo?.total ?? state[USER_LIST_SCOPE].length,
};
state.pageInfo = {
...state.pageInfo,
[USER_LIST_SCOPE]: paginationInfo,
};
},
[types.RECEIVE_USER_LISTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.REQUEST_ROTATE_INSTANCE_ID](state) {
state.isRotating = true;
state.hasRotateError = false;
......@@ -90,18 +61,9 @@ export default {
updateFlag(state, mapFlag(data));
},
[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, i) {
const flag = state[FEATURE_FLAG_SCOPE].find(({ id }) => i === id);
const flag = state.featureFlags.find(({ id }) => i === id);
updateFlag(state, { ...flag, active: !flag.active });
},
[types.REQUEST_DELETE_USER_LIST](state, list) {
state.userLists = state.userLists.filter((l) => l !== list);
},
[types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) {
state.isLoading = false;
state.hasError = false;
state.alerts = [].concat(error.message);
state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid);
},
[types.RECEIVE_CLEAR_ALERT](state, index) {
state.alerts.splice(index, 1);
},
......
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../constants';
export default ({ endpoint, projectId, unleashApiInstanceId, rotateInstanceIdPath }) => ({
[FEATURE_FLAG_SCOPE]: [],
[USER_LIST_SCOPE]: [],
featureFlags: [],
alerts: [],
count: {},
pageInfo: { [FEATURE_FLAG_SCOPE]: {}, [USER_LIST_SCOPE]: {} },
count: 0,
pageInfo: {},
isLoading: true,
hasError: false,
endpoint,
......
/* eslint-disable no-new */
import Vue from 'vue';
import Vuex from 'vuex';
import UserLists from '~/user_lists/components/user_lists.vue';
import createStore from '~/user_lists/store/index';
Vue.use(Vuex);
const el = document.querySelector('#js-user-lists');
const { featureFlagsHelpPagePath, errorStateSvgPath, projectId, newUserListPath } = el.dataset;
new Vue({
el,
store: createStore({ projectId }),
provide: {
featureFlagsHelpPagePath,
errorStateSvgPath,
newUserListPath,
},
render(createElement) {
return createElement(UserLists);
},
});
<script>
import { GlBadge, GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapState, mapActions } from 'vuex';
import EmptyState from '~/feature_flags/components/empty_state.vue';
import {
buildUrlWithCurrentLocation,
getParameterByName,
historyPushState,
} from '~/lib/utils/common_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import UserListsTable from './user_lists_table.vue';
export default {
components: {
EmptyState,
UserListsTable,
GlBadge,
GlButton,
TablePagination,
},
inject: {
newUserListPath: { default: '' },
},
data() {
return {
page: getParameterByName('page') || '1',
};
},
computed: {
...mapState(['userLists', 'alerts', 'count', 'pageInfo', 'isLoading', 'hasError', 'options']),
canUserRotateToken() {
return this.rotateInstanceIdPath !== '';
},
shouldRenderPagination() {
return (
!this.isLoading &&
!this.hasError &&
this.userLists.length > 0 &&
this.pageInfo.total > this.pageInfo.perPage
);
},
shouldShowEmptyState() {
return !this.isLoading && !this.hasError && this.userLists.length === 0;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
shouldRenderUserLists() {
return !this.isLoading && this.userLists.length > 0 && !this.hasError;
},
hasNewPath() {
return !isEmpty(this.newUserListPath);
},
},
created() {
this.setUserListsOptions({ page: this.page });
this.fetchUserLists();
},
methods: {
...mapActions(['setUserListsOptions', 'fetchUserLists', 'clearAlert', 'deleteUserList']),
onChangePage(page) {
this.updateUserListsOptions({
/* URLS parameters are strings, we need to parse to match types */
page: Number(page).toString(),
});
},
updateUserListsOptions(parameters) {
const queryString = objectToQuery(parameters);
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.setUserListsOptions(parameters);
this.fetchUserLists();
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-flex-direction-column gl-md-display-none!">
<gl-button v-if="hasNewPath" :href="newUserListPath" variant="confirm">
{{ s__('UserLists|New user list') }}
</gl-button>
</div>
<div
class="gl-display-flex gl-align-items-baseline gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between gl-mt-6"
>
<div class="gl-display-flex gl-align-items-center">
<h2 class="gl-font-size-h2 gl-my-0">
{{ s__('UserLists|User Lists') }}
</h2>
<gl-badge v-if="count" class="gl-ml-4">{{ count }}</gl-badge>
</div>
<div class="gl-display-flex gl-align-items-center gl-justify-content-end">
<gl-button v-if="hasNewPath" :href="newUserListPath" variant="confirm">
{{ s__('UserLists|New user list') }}
</gl-button>
</div>
</div>
<empty-state
:alerts="alerts"
:is-loading="isLoading"
:loading-label="s__('UserLists|Loading user lists')"
:error-state="shouldRenderErrorState"
:error-title="s__('UserLists|There was an error fetching the user lists.')"
:empty-state="shouldShowEmptyState"
:empty-title="s__('UserLists|Get started with user lists')"
:empty-description="
s__('UserLists|User lists allow you to define a set of users to use with Feature Flags.')
"
@dismissAlert="clearAlert"
>
<user-lists-table :user-lists="userLists" @delete="deleteUserList" />
</empty-state>
</div>
<table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="pageInfo" />
</div>
</template>
import Api from '~/api';
import * as types from './mutation_types';
export const setUserListsOptions = ({ commit }, options) =>
commit(types.SET_USER_LISTS_OPTIONS, options);
export const fetchUserLists = ({ state, dispatch }) => {
dispatch('requestUserLists');
return Api.fetchFeatureFlagUserLists(state.projectId, state.options.page)
.then(({ data, headers }) => dispatch('receiveUserListsSuccess', { data, headers }))
.catch(() => dispatch('receiveUserListsError'));
};
export const requestUserLists = ({ commit }) => commit(types.REQUEST_USER_LISTS);
export const receiveUserListsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_USER_LISTS_SUCCESS, response);
export const receiveUserListsError = ({ commit }) => commit(types.RECEIVE_USER_LISTS_ERROR);
export const deleteUserList = ({ state, dispatch }, list) => {
dispatch('requestDeleteUserList', list);
return Api.deleteFeatureFlagUserList(state.projectId, list.iid)
.then(() => dispatch('fetchUserLists'))
.catch((error) =>
dispatch('receiveDeleteUserListError', {
list,
error: error?.response?.data ?? error,
}),
);
};
export const requestDeleteUserList = ({ commit }, list) =>
commit(types.REQUEST_DELETE_USER_LIST, list);
export const receiveDeleteUserListError = ({ commit }, { error, list }) =>
commit(types.RECEIVE_DELETE_USER_LIST_ERROR, { error, list });
export const clearAlert = ({ commit }, index) => commit(types.RECEIVE_CLEAR_ALERT, index);
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
export default (initialState) =>
new Vuex.Store({
actions,
mutations,
state: createState(initialState),
});
export const SET_USER_LISTS_OPTIONS = 'SET_FEATURE_FLAGS_OPTIONS';
export const REQUEST_USER_LISTS = 'REQUEST_USER_LISTS';
export const RECEIVE_USER_LISTS_SUCCESS = 'RECEIVE_USER_LISTS_SUCCESS';
export const RECEIVE_USER_LISTS_ERROR = 'RECEIVE_USER_LISTS_ERROR';
export const REQUEST_DELETE_USER_LIST = 'REQUEST_DELETE_USER_LIST';
export const RECEIVE_DELETE_USER_LIST_ERROR = 'RECEIVE_DELETE_USER_LIST_ERROR';
export const RECEIVE_CLEAR_ALERT = 'RECEIVE_CLEAR_ALERT';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
export default {
[types.SET_USER_LISTS_OPTIONS](state, options = {}) {
state.options = options;
},
[types.REQUEST_USER_LISTS](state) {
state.isLoading = true;
},
[types.RECEIVE_USER_LISTS_SUCCESS](state, { data, headers }) {
state.isLoading = false;
state.hasError = false;
state.userLists = data || [];
const normalizedHeaders = normalizeHeaders(headers);
const paginationInfo = parseIntPagination(normalizedHeaders);
state.count = paginationInfo?.total ?? state.userLists.length;
state.pageInfo = paginationInfo;
},
[types.RECEIVE_USER_LISTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.REQUEST_DELETE_USER_LIST](state, list) {
state.userLists = state.userLists.filter((l) => l !== list);
},
[types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) {
state.isLoading = false;
state.hasError = false;
state.alerts = [].concat(error.message);
state.userLists = state.userLists.concat(list).sort((l1, l2) => l1.iid - l2.iid);
},
[types.RECEIVE_CLEAR_ALERT](state, index) {
state.alerts.splice(index, 1);
},
};
export default ({ projectId }) => ({
userLists: [],
alerts: [],
count: 0,
pageInfo: {},
isLoading: true,
hasError: false,
options: {},
projectId,
});
......@@ -6,6 +6,9 @@ class Projects::FeatureFlagsUserListsController < Projects::ApplicationControlle
feature_category :feature_flags
def index
end
def new
end
......
......@@ -14,4 +14,4 @@
"can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
"new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil,
"rotate-instance-id-path" => can?(current_user, :admin_feature_flags_client, @project) ? reset_token_project_feature_flags_client_path(@project, format: :json) : nil,
"new-user-list-path" => can?(current_user, :admin_feature_flags_user_lists, @project) ? new_project_feature_flags_user_list_path(@project) : nil } }
"user-list-path" => can?(current_user, :admin_feature_flags_user_lists, @project) ? project_feature_flags_user_lists_path(@project) : nil } }
- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
- add_to_breadcrumbs s_('FeatureFlags|User Lists'), project_feature_flags_user_lists_path(@project)
- breadcrumb_title s_('FeatureFlags|Edit User List')
- page_title s_('FeatureFlags|Edit User List')
......
- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
- breadcrumb_title s_('FeatureFlags|User Lists')
- page_title s_('FeatureFlags|Feature Flag User Lists')
#js-user-lists{ data: { project_id: @project.id,
feature_flags_help_page_path: help_page_path("operations/feature_flags"),
new_user_list_path: can?(current_user, :create_feature_flag, @project) ? new_project_feature_flags_user_list_path(@project): nil,
error_state_svg_path: image_path('illustrations/feature_flag.svg') } }
- @breadcrumb_link = new_project_feature_flags_user_list_path(@project)
- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
- add_to_breadcrumbs s_('FeatureFlags|User Lists'), project_feature_flags_user_lists_path(@project)
- breadcrumb_title s_('FeatureFlags|New User List')
- page_title s_('FeatureFlags|New User List')
......
- add_to_breadcrumbs s_('FeatureFlags|Feature Flags'), project_feature_flags_path(@project)
- add_to_breadcrumbs s_('FeatureFlags|User Lists'), project_feature_flags_user_lists_path(@project)
- breadcrumb_title s_('FeatureFlags|List details')
- page_title s_('FeatureFlags|Feature Flag User List Details')
......
......@@ -403,7 +403,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :feature_flags_client, only: [] do
post :reset_token
end
resources :feature_flags_user_lists, param: :iid, only: [:new, :edit, :show]
resources :feature_flags_user_lists, param: :iid, only: [:index, :new, :edit, :show]
get '/schema/:branch/*filename',
to: 'web_ide_schemas#show',
......
......@@ -184,14 +184,16 @@ For example:
#### Create a user list
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13308) in GitLab 13.3.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13308) in GitLab 13.3.
> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/322425) in GitLab 14.0.
To create a user list:
1. In your project, navigate to **Operations > Feature Flags**.
1. Click on **New list**.
1. Select **View user lists**
1. Select **New user list**.
1. Enter a name for the list.
1. Click **Create**.
1. Select **Create**.
You can view a list's User IDs by clicking the **{pencil}** (edit) button next to it.
When viewing a list, you can rename it by clicking the **Edit** button.
......
......@@ -13842,6 +13842,9 @@ msgstr ""
msgid "FeatureFlags|Feature Flag User List Details"
msgstr ""
msgid "FeatureFlags|Feature Flag User Lists"
msgstr ""
msgid "FeatureFlags|Feature Flag behavior is built up by creating a set of rules to define the status of target environments. A default wildcard rule %{codeStart}*%{codeEnd} for %{boldStart}All Environments%{boldEnd} is set, and you are able to add as many rules as you need by choosing environment specs below. You can toggle the behavior for each of your rules to set them %{boldStart}Active%{boldEnd} or %{boldStart}Inactive%{boldEnd}."
msgstr ""
......@@ -13866,9 +13869,6 @@ msgstr ""
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
msgid "FeatureFlags|Get started with user lists"
msgstr ""
msgid "FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag."
msgstr ""
......@@ -13896,9 +13896,6 @@ msgstr ""
msgid "FeatureFlags|Loading feature flags"
msgstr ""
msgid "FeatureFlags|Loading user lists"
msgstr ""
msgid "FeatureFlags|More information"
msgstr ""
......@@ -13917,9 +13914,6 @@ msgstr ""
msgid "FeatureFlags|New feature flag"
msgstr ""
msgid "FeatureFlags|New user list"
msgstr ""
msgid "FeatureFlags|No user list selected"
msgstr ""
......@@ -13962,9 +13956,6 @@ msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr ""
msgid "FeatureFlags|There was an error fetching the user lists."
msgstr ""
msgid "FeatureFlags|To prevent accidental actions we ask you to confirm your intention. Please type %{projectName} to proceed or close this modal to cancel."
msgstr ""
......@@ -13980,7 +13971,7 @@ msgstr ""
msgid "FeatureFlags|User Lists"
msgstr ""
msgid "FeatureFlags|User lists allow you to define a set of users to use with Feature Flags."
msgid "FeatureFlags|View user lists"
msgstr ""
msgid "FeatureFlag|Percentage"
......@@ -35536,27 +35527,45 @@ msgstr ""
msgid "UserLists|Feature flag user list"
msgstr ""
msgid "UserLists|Get started with user lists"
msgstr ""
msgid "UserLists|Lists allow you to define a set of users to be used with feature flags. %{linkStart}Read more about feature flag lists.%{linkEnd}"
msgstr ""
msgid "UserLists|Loading user lists"
msgstr ""
msgid "UserLists|Name"
msgstr ""
msgid "UserLists|New list"
msgstr ""
msgid "UserLists|New user list"
msgstr ""
msgid "UserLists|Save"
msgstr ""
msgid "UserLists|There are no users"
msgstr ""
msgid "UserLists|There was an error fetching the user lists."
msgstr ""
msgid "UserLists|User ID"
msgstr ""
msgid "UserLists|User IDs"
msgstr ""
msgid "UserLists|User Lists"
msgstr ""
msgid "UserLists|User lists allow you to define a set of users to use with Feature Flags."
msgstr ""
msgid "UserList|Delete %{name}?"
msgstr ""
......
......@@ -16,6 +16,39 @@ RSpec.describe Projects::FeatureFlagsUserListsController do
{ namespace_id: project.namespace, project_id: project }.merge(extra_params)
end
describe 'GET #index' do
it 'redirects when the user is unauthenticated' do
get(:index, params: request_params)
expect(response).to redirect_to(new_user_session_path)
end
it 'returns not found if the user does not belong to the project' do
user = create(:user)
sign_in(user)
get(:index, params: request_params)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns not found for a reporter' do
sign_in(reporter)
get(:index, params: request_params)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'renders the new page for a developer' do
sign_in(developer)
get(:index, params: request_params)
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'GET #new' do
it 'redirects when the user is unauthenticated' do
get(:new, params: request_params)
......
......@@ -17,12 +17,13 @@ RSpec.describe 'User deletes feature flag user list', :js do
end
it 'deletes the list' do
visit(project_feature_flags_path(project, scope: 'userLists'))
visit(project_feature_flags_user_lists_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
expect(page).to have_text('Lists 0')
expect(page).to have_text('Lists')
expect(page).not_to have_selector('[data-testid="ffUserListName"]')
end
end
......@@ -34,7 +35,7 @@ RSpec.describe 'User deletes feature flag user list', :js do
end
it 'does not delete the list' do
visit(project_feature_flags_path(project, scope: 'userLists'))
visit(project_feature_flags_user_lists_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
......
import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
import EmptyState from '~/feature_flags/components/empty_state.vue';
const DEFAULT_PROPS = {
title: 'test',
count: 5,
alerts: ['an alert', 'another alert'],
isLoading: false,
loadingLabel: 'test loading',
errorState: false,
errorTitle: 'test title',
emptyState: true,
emptyState: false,
emptyTitle: 'test empty',
emptyDescription: 'empty description',
};
......@@ -27,13 +25,10 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
mount(
{
components: {
GlTabs,
FeatureFlagsTab,
EmptyState,
},
render(h) {
return h(GlTabs, [
h(FeatureFlagsTab, { props: this.$attrs, on: this.$listeners }, this.$slots.default),
]);
return h(EmptyState, { props: this.$attrs, on: this.$listeners }, this.$slots.default);
},
},
{
......@@ -72,7 +67,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
it('should emit a dismiss event for a dismissed alert', () => {
alerts.at(0).vm.$emit('dismiss');
expect(wrapper.find(FeatureFlagsTab).emitted('dismissAlert')).toEqual([[0]]);
expect(wrapper.find(EmptyState).emitted('dismissAlert')).toEqual([[0]]);
});
});
......@@ -138,30 +133,4 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
expect(slot.text()).toBe('testing');
});
});
describe('count', () => {
it('should display a count if there is one', async () => {
wrapper = factory();
await wrapper.vm.$nextTick();
expect(wrapper.find(GlBadge).text()).toBe(DEFAULT_PROPS.count.toString());
});
it('should display 0 if there is no count', async () => {
wrapper = factory({ count: undefined });
await wrapper.vm.$nextTick();
expect(wrapper.find(GlBadge).text()).toBe('0');
});
});
describe('title', () => {
it('should show the title', async () => {
wrapper = factory();
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="feature-flags-tab-title"]').text()).toBe(
DEFAULT_PROPS.title,
);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
import {
requestFeatureFlags,
......@@ -17,18 +16,12 @@ import {
updateFeatureFlag,
receiveUpdateFeatureFlagSuccess,
receiveUpdateFeatureFlagError,
requestUserLists,
receiveUserListsSuccess,
receiveUserListsError,
fetchUserLists,
deleteUserList,
receiveDeleteUserListError,
clearAlert,
} from '~/feature_flags/store/index/actions';
import * as types from '~/feature_flags/store/index/mutation_types';
import state from '~/feature_flags/store/index/state';
import axios from '~/lib/utils/axios_utils';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
jest.mock('~/api.js');
......@@ -154,99 +147,6 @@ describe('Feature flags actions', () => {
});
});
describe('fetchUserLists', () => {
beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} });
});
describe('success', () => {
it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => {
testAction(
fetchUserLists,
null,
mockedState,
[],
[
{
type: 'requestUserLists',
},
{
payload: { data: [userList], headers: {} },
type: 'receiveUserListsSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestUserLists and receiveUserListsError ', (done) => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
testAction(
fetchUserLists,
null,
mockedState,
[],
[
{
type: 'requestUserLists',
},
{
type: 'receiveUserListsError',
},
],
done,
);
});
});
});
describe('requestUserLists', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
testAction(
requestUserLists,
null,
mockedState,
[{ type: types.REQUEST_USER_LISTS }],
[],
done,
);
});
});
describe('receiveUserListsSuccess', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
testAction(
receiveUserListsSuccess,
{ data: [userList], headers: {} },
mockedState,
[
{
type: types.RECEIVE_USER_LISTS_SUCCESS,
payload: { data: [userList], headers: {} },
},
],
[],
done,
);
});
});
describe('receiveUserListsError', () => {
it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => {
testAction(
receiveUserListsError,
null,
mockedState,
[{ type: types.RECEIVE_USER_LISTS_ERROR }],
[],
done,
);
});
});
describe('rotateInstanceId', () => {
let mock;
......@@ -482,69 +382,6 @@ describe('Feature flags actions', () => {
);
});
});
describe('deleteUserList', () => {
beforeEach(() => {
mockedState.userLists = [userList];
});
describe('success', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockResolvedValue();
});
it('should refresh the user lists', (done) => {
testAction(
deleteUserList,
userList,
mockedState,
[],
[{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
});
it('should dispatch receiveDeleteUserListError', (done) => {
testAction(
deleteUserList,
userList,
mockedState,
[],
[
{ type: 'requestDeleteUserList', payload: userList },
{
type: 'receiveDeleteUserListError',
payload: { list: userList, error: 'some error' },
},
],
done,
);
});
});
});
describe('receiveDeleteUserListError', () => {
it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => {
testAction(
receiveDeleteUserListError,
{ list: userList, error: 'mock error' },
mockedState,
[
{
type: 'RECEIVE_DELETE_USER_LIST_ERROR',
payload: { list: userList, error: 'mock error' },
},
],
[],
done,
);
});
});
describe('clearAlert', () => {
it('should commit RECEIVE_CLEAR_ALERT', (done) => {
......
......@@ -3,7 +3,7 @@ import * as types from '~/feature_flags/store/index/mutation_types';
import mutations from '~/feature_flags/store/index/mutations';
import state from '~/feature_flags/store/index/state';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
describe('Feature flags store Mutations', () => {
let stateCopy;
......@@ -59,13 +59,11 @@ describe('Feature flags store Mutations', () => {
});
it('should set count with the given data', () => {
expect(stateCopy.count.featureFlags).toEqual(37);
expect(stateCopy.count).toEqual(37);
});
it('should set pagination', () => {
expect(stateCopy.pageInfo.featureFlags).toEqual(
parseIntPagination(normalizeHeaders(headers)),
);
expect(stateCopy.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
});
......@@ -83,58 +81,6 @@ describe('Feature flags store Mutations', () => {
});
});
describe('REQUEST_USER_LISTS', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_USER_LISTS](stateCopy);
expect(stateCopy.isLoading).toBe(true);
});
});
describe('RECEIVE_USER_LISTS_SUCCESS', () => {
const headers = {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
};
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_SUCCESS](stateCopy, { data: [userList], headers });
});
it('sets isLoading to false', () => {
expect(stateCopy.isLoading).toBe(false);
});
it('sets userLists to the received userLists', () => {
expect(stateCopy.userLists).toEqual([userList]);
});
it('sets pagination info for user lits', () => {
expect(stateCopy.pageInfo.userLists).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
it('sets the count for user lists', () => {
expect(stateCopy.count.userLists).toBe(parseInt(headers['X-TOTAL'], 10));
});
});
describe('RECEIVE_USER_LISTS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_ERROR](stateCopy);
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(stateCopy.hasError).toEqual(true);
});
});
describe('REQUEST_ROTATE_INSTANCE_ID', () => {
beforeEach(() => {
mutations[types.REQUEST_ROTATE_INSTANCE_ID](stateCopy);
......@@ -214,7 +160,7 @@ describe('Feature flags store Mutations', () => {
...flagState,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count.featureFlags = stateCount;
stateCopy.count = stateCount;
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](stateCopy, {
...featureFlag,
......@@ -241,8 +187,6 @@ describe('Feature flags store Mutations', () => {
...flag,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count = { enabled: 1, disabled: 0 };
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](stateCopy, featureFlag.id);
});
......@@ -257,36 +201,6 @@ describe('Feature flags store Mutations', () => {
});
});
describe('REQUEST_DELETE_USER_LIST', () => {
beforeEach(() => {
stateCopy.userLists = [userList];
mutations[types.REQUEST_DELETE_USER_LIST](stateCopy, userList);
});
it('should remove the deleted list', () => {
expect(stateCopy.userLists).not.toContain(userList);
});
});
describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
beforeEach(() => {
stateCopy.userLists = [];
mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, {
list: userList,
error: 'some error',
});
});
it('should set isLoading to false and hasError to false', () => {
expect(stateCopy.isLoading).toBe(false);
expect(stateCopy.hasError).toBe(false);
});
it('should add the user list back to the list of user lists', () => {
expect(stateCopy.userLists).toContain(userList);
});
});
describe('RECEIVE_CLEAR_ALERT', () => {
it('clears the alert', () => {
stateCopy.alerts = ['a server error'];
......
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import UserListsComponent from '~/user_lists/components/user_lists.vue';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
import createStore from '~/user_lists/store/index';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
Vue.use(Vuex);
describe('~/user_lists/components/user_lists.vue', () => {
const mockProvide = {
newUserListPath: '/user-lists/new',
featureFlagsHelpPagePath: '/help/feature-flags',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
};
const mockState = {
projectId: '1',
};
let wrapper;
let store;
const factory = (provide = mockProvide, fn = mount) => {
store = createStore(mockState);
wrapper = fn(UserListsComponent, {
store,
provide,
});
};
const newButton = () => within(wrapper.element).queryAllByText('New user list');
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('without permissions', () => {
const provideData = {
...mockProvide,
newUserListPath: null,
};
beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} });
factory(provideData);
});
it('does not render new user list button', () => {
expect(newButton()).toHaveLength(0);
});
});
describe('loading state', () => {
it('renders a loading icon', () => {
Api.fetchFeatureFlagUserLists.mockReturnValue(new Promise(() => {}));
factory();
const loadingElement = wrapper.findComponent(GlLoadingIcon);
expect(loadingElement.exists()).toBe(true);
expect(loadingElement.props('label')).toEqual('Loading user lists');
});
});
describe('successful request', () => {
describe('without user lists', () => {
let emptyState;
beforeEach(async () => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} });
factory();
await waitForPromises();
await Vue.nextTick();
emptyState = wrapper.findComponent(GlEmptyState);
});
it('should render the empty state', async () => {
expect(emptyState.exists()).toBe(true);
});
it('renders new feature flag button', () => {
expect(newButton()).not.toHaveLength(0);
});
it('renders generic title', () => {
const title = createWrapper(
within(emptyState.element).getByText('Get started with user lists'),
);
expect(title.exists()).toBe(true);
});
it('renders generic description', () => {
const description = createWrapper(
within(emptyState.element).getByText(
'User lists allow you to define a set of users to use with Feature Flags.',
),
);
expect(description.exists()).toBe(true);
});
});
describe('with paginated user lists', () => {
let table;
beforeEach(async () => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({
data: [userList],
headers: {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
},
});
factory();
jest.spyOn(store, 'dispatch');
await Vue.nextTick();
table = wrapper.findComponent(UserListsTable);
});
it('should render a table with feature flags', () => {
expect(table.exists()).toBe(true);
expect(table.props('userLists')).toEqual([userList]);
});
it('renders new feature flag button', () => {
expect(newButton()).not.toHaveLength(0);
});
describe('pagination', () => {
let pagination;
beforeEach(() => {
pagination = wrapper.findComponent(TablePagination);
});
it('should render pagination', () => {
expect(pagination.exists()).toBe(true);
});
it('should make an API request when page is clicked', () => {
jest.spyOn(store, 'dispatch');
pagination.vm.change('4');
expect(store.dispatch).toHaveBeenCalledWith('setUserListsOptions', {
page: '4',
});
});
});
});
});
describe('unsuccessful request', () => {
beforeEach(async () => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
factory();
await Vue.nextTick();
});
it('should render error state', () => {
const emptyState = wrapper.findComponent(GlEmptyState);
const title = createWrapper(
within(emptyState.element).getByText('There was an error fetching the user lists.'),
);
expect(title.exists()).toBe(true);
const description = createWrapper(
within(emptyState.element).getByText(
'Try again in a few moments or contact your support team.',
),
);
expect(description.exists()).toBe(true);
});
it('renders new feature flag button', () => {
expect(newButton()).not.toHaveLength(0);
});
});
});
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
import UserListsTable from '~/feature_flags/components/user_lists_table.vue';
import { userList } from '../mock_data';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
import { userList } from '../../feature_flags/mock_data';
jest.mock('timeago.js', () => ({
format: jest.fn().mockReturnValue('2 weeks ago'),
......
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import {
setUserListsOptions,
requestUserLists,
receiveUserListsSuccess,
receiveUserListsError,
fetchUserLists,
deleteUserList,
receiveDeleteUserListError,
clearAlert,
} from '~/user_lists/store/index/actions';
import * as types from '~/user_lists/store/index/mutation_types';
import createState from '~/user_lists/store/index/state';
import { userList } from '../../../feature_flags/mock_data';
jest.mock('~/api.js');
describe('~/user_lists/store/index/actions', () => {
let state;
beforeEach(() => {
state = createState({ projectId: '1' });
});
describe('setUserListsOptions', () => {
it('should commit SET_USER_LISTS_OPTIONS mutation', (done) => {
testAction(
setUserListsOptions,
{ page: '1', scope: 'all' },
state,
[{ type: types.SET_USER_LISTS_OPTIONS, payload: { page: '1', scope: 'all' } }],
[],
done,
);
});
});
describe('fetchUserLists', () => {
beforeEach(() => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} });
});
describe('success', () => {
it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => {
testAction(
fetchUserLists,
null,
state,
[],
[
{
type: 'requestUserLists',
},
{
payload: { data: [userList], headers: {} },
type: 'receiveUserListsSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestUserLists and receiveUserListsError ', (done) => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
testAction(
fetchUserLists,
null,
state,
[],
[
{
type: 'requestUserLists',
},
{
type: 'receiveUserListsError',
},
],
done,
);
});
});
});
describe('requestUserLists', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
testAction(requestUserLists, null, state, [{ type: types.REQUEST_USER_LISTS }], [], done);
});
});
describe('receiveUserListsSuccess', () => {
it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
testAction(
receiveUserListsSuccess,
{ data: [userList], headers: {} },
state,
[
{
type: types.RECEIVE_USER_LISTS_SUCCESS,
payload: { data: [userList], headers: {} },
},
],
[],
done,
);
});
});
describe('receiveUserListsError', () => {
it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => {
testAction(
receiveUserListsError,
null,
state,
[{ type: types.RECEIVE_USER_LISTS_ERROR }],
[],
done,
);
});
});
describe('deleteUserList', () => {
beforeEach(() => {
state.userLists = [userList];
});
describe('success', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockResolvedValue();
});
it('should refresh the user lists', (done) => {
testAction(
deleteUserList,
userList,
state,
[],
[{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
});
it('should dispatch receiveDeleteUserListError', (done) => {
testAction(
deleteUserList,
userList,
state,
[],
[
{ type: 'requestDeleteUserList', payload: userList },
{
type: 'receiveDeleteUserListError',
payload: { list: userList, error: 'some error' },
},
],
done,
);
});
});
});
describe('receiveDeleteUserListError', () => {
it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => {
testAction(
receiveDeleteUserListError,
{ list: userList, error: 'mock error' },
state,
[
{
type: 'RECEIVE_DELETE_USER_LIST_ERROR',
payload: { list: userList, error: 'mock error' },
},
],
[],
done,
);
});
});
describe('clearAlert', () => {
it('should commit RECEIVE_CLEAR_ALERT', (done) => {
const alertIndex = 3;
testAction(
clearAlert,
alertIndex,
state,
[{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }],
[],
done,
);
});
});
});
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from '~/user_lists/store/index/mutation_types';
import mutations from '~/user_lists/store/index/mutations';
import createState from '~/user_lists/store/index/state';
import { userList } from '../../../feature_flags/mock_data';
describe('~/user_lists/store/index/mutations', () => {
let state;
beforeEach(() => {
state = createState({ projectId: '1' });
});
describe('SET_USER_LISTS_OPTIONS', () => {
it('should set provided options', () => {
mutations[types.SET_USER_LISTS_OPTIONS](state, { page: '1', scope: 'all' });
expect(state.options).toEqual({ page: '1', scope: 'all' });
});
});
describe('REQUEST_USER_LISTS', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_USER_LISTS](state);
expect(state.isLoading).toBe(true);
});
});
describe('RECEIVE_USER_LISTS_SUCCESS', () => {
const headers = {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
};
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_SUCCESS](state, { data: [userList], headers });
});
it('sets isLoading to false', () => {
expect(state.isLoading).toBe(false);
});
it('sets userLists to the received userLists', () => {
expect(state.userLists).toEqual([userList]);
});
it('sets pagination info for user lits', () => {
expect(state.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
it('sets the count for user lists', () => {
expect(state.count).toBe(parseInt(headers['X-TOTAL'], 10));
});
});
describe('RECEIVE_USER_LISTS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_USER_LISTS_ERROR](state);
});
it('should set isLoading to false', () => {
expect(state.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(state.hasError).toEqual(true);
});
});
describe('REQUEST_DELETE_USER_LIST', () => {
beforeEach(() => {
state.userLists = [userList];
mutations[types.REQUEST_DELETE_USER_LIST](state, userList);
});
it('should remove the deleted list', () => {
expect(state.userLists).not.toContain(userList);
});
});
describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
beforeEach(() => {
state.userLists = [];
mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](state, {
list: userList,
error: 'some error',
});
});
it('should set isLoading to false and hasError to false', () => {
expect(state.isLoading).toBe(false);
expect(state.hasError).toBe(false);
});
it('should add the user list back to the list of user lists', () => {
expect(state.userLists).toContain(userList);
});
});
describe('RECEIVE_CLEAR_ALERT', () => {
it('clears the alert', () => {
state.alerts = ['a server error'];
mutations[types.RECEIVE_CLEAR_ALERT](state, 0);
expect(state.alerts).toEqual([]);
});
it('clears the alert at the specified index', () => {
state.alerts = ['a server error', 'another error', 'final error'];
mutations[types.RECEIVE_CLEAR_ALERT](state, 1);
expect(state.alerts).toEqual(['a server error', 'final error']);
});
});
});
......@@ -90,6 +90,5 @@ module FeatureFlagHelpers
def expect_user_to_see_feature_flags_index_page
expect(page).to have_text('Feature Flags')
expect(page).to have_text('Lists')
end
end
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