Commit 10e90560 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Paul Slaughter

Add Components for Adding and Editing User Lists

Once these are mounted, users will be able to create new user lists and
edit existing ones.
parent 7ab84ce0
<script>
import { mapActions, mapState } from 'vuex';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import statuses from '../constants/edit';
import UserListForm from './user_list_form.vue';
export default {
components: {
GlAlert,
GlLoadingIcon,
UserListForm,
},
props: {
userListsDocsPath: {
type: String,
required: true,
},
},
translations: {
saveButtonLabel: s__('UserLists|Save'),
},
computed: {
...mapState(['userList', 'status', 'errorMessage']),
title() {
return sprintf(s__('UserLists|Edit %{name}'), { name: this.userList?.name });
},
isLoading() {
return this.status === statuses.LOADING;
},
isError() {
return this.status === statuses.ERROR;
},
hasUserList() {
return Boolean(this.userList);
},
},
mounted() {
this.fetchUserList();
},
methods: {
...mapActions(['fetchUserList', 'updateUserList', 'dismissErrorAlert']),
},
};
</script>
<template>
<div>
<gl-alert
v-if="isError"
:dismissible="hasUserList"
variant="danger"
@dismiss="dismissErrorAlert"
>
<ul class="gl-mb-0">
<li v-for="(message, index) in errorMessage" :key="index">
{{ message }}
</li>
</ul>
</gl-alert>
<gl-loading-icon v-if="isLoading" size="xl" />
<template v-else-if="hasUserList">
<h3
data-testid="user-list-title"
class="gl-font-weight-bold gl-pb-5 gl-border-b-solid gl-border-gray-100 gl-border-1"
>
{{ title }}
</h3>
<user-list-form
:cancel-path="userList.path"
:save-button-label="$options.translations.saveButtonLabel"
:user-lists-docs-path="userListsDocsPath"
:user-list="userList"
@submit="updateUserList"
/>
</template>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import UserListForm from './user_list_form.vue';
export default {
components: {
GlAlert,
UserListForm,
},
props: {
featureFlagsPath: {
type: String,
required: true,
},
userListsDocsPath: {
type: String,
required: true,
},
},
translations: {
pageTitle: s__('UserLists|New list'),
createButtonLabel: s__('UserLists|Create'),
},
computed: {
...mapState(['userList', 'errorMessage']),
isError() {
return Array.isArray(this.errorMessage) && this.errorMessage.length > 0;
},
},
methods: {
...mapActions(['createUserList', 'dismissErrorAlert']),
},
};
</script>
<template>
<div>
<gl-alert v-if="isError" variant="danger" @dismiss="dismissErrorAlert">
<ul class="gl-mb-0">
<li v-for="(message, index) in errorMessage" :key="index">
{{ message }}
</li>
</ul>
</gl-alert>
<h3 class="gl-font-weight-bold gl-pb-5 gl-border-b-solid gl-border-gray-100 gl-border-1">
{{ $options.translations.pageTitle }}
</h3>
<user-list-form
:cancel-path="featureFlagsPath"
:save-button-label="$options.translations.createButtonLabel"
:user-lists-docs-path="userListsDocsPath"
:user-list="userList"
@submit="createUserList"
/>
</div>
</template>
<script>
import { GlButton, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlLink,
GlSprintf,
},
props: {
cancelPath: {
type: String,
required: true,
},
saveButtonLabel: {
type: String,
required: true,
},
userListsDocsPath: {
type: String,
required: true,
},
userList: {
type: Object,
required: true,
},
},
classes: {
actionContainer: [
'gl-py-5',
'gl-display-flex',
'gl-justify-content-space-between',
'gl-px-4',
'gl-border-t-solid',
'gl-border-gray-100',
'gl-border-1',
'gl-bg-gray-10',
],
},
translations: {
formLabel: s__('UserLists|Feature flag list'),
formSubtitle: s__(
'UserLists|Lists allow you to define a set of users to be used with feature flags. %{linkStart}Read more about feature flag lists.%{linkEnd}',
),
nameLabel: s__('UserLists|Name'),
cancelButtonLabel: s__('UserLists|Cancel'),
},
data() {
return {
name: this.userList.name,
};
},
methods: {
submit() {
this.$emit('submit', { ...this.userList, name: this.name });
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-mt-7">
<div class="gl-flex-basis-0 gl-mr-7">
<h4 class="gl-min-width-fit-content gl-white-space-nowrap">
{{ $options.translations.formLabel }}
</h4>
<gl-sprintf :message="$options.translations.formSubtitle" class="gl-text-gray-700">
<template #link="{ content }">
<gl-link :href="userListsDocsPath" data-testid="user-list-docs-link">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-flex-fill-1 gl-ml-7">
<gl-form-group :label="$options.translations.nameLabel" class="gl-mb-7">
<gl-form-input v-model="name" data-testid="user-list-name" required />
</gl-form-group>
<div :class="$options.classes.actionContainer">
<gl-button variant="success" data-testid="save-user-list" @click="submit">
{{ saveButtonLabel }}
</gl-button>
<gl-button :href="cancelPath" data-testid="user-list-cancel">
{{ $options.translations.cancelButtonLabel }}
</gl-button>
</div>
</div>
</div>
</div>
</template>
...@@ -14,8 +14,7 @@ export const dismissErrorAlert = ({ commit }) => commit(types.DISMISS_ERROR_ALER ...@@ -14,8 +14,7 @@ export const dismissErrorAlert = ({ commit }) => commit(types.DISMISS_ERROR_ALER
export const updateUserList = ({ commit, state }, userList) => { export const updateUserList = ({ commit, state }, userList) => {
return Api.updateFeatureFlagUserList(state.projectId, { return Api.updateFeatureFlagUserList(state.projectId, {
...state.userList, name: userList.name,
...userList,
}) })
.then(({ data }) => redirectTo(data.path)) .then(({ data }) => redirectTo(data.path))
.catch(response => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response))); .catch(response => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response)));
......
...@@ -5,5 +5,5 @@ export default ({ projectId = '', userListIid = '' }) => ({ ...@@ -5,5 +5,5 @@ export default ({ projectId = '', userListIid = '' }) => ({
projectId, projectId,
userListIid, userListIid,
userList: null, userList: null,
errorMessage: '', errorMessage: [],
}); });
export default ({ projectId = '' }) => ({ export default ({ projectId = '' }) => ({
projectId, projectId,
userList: { name: '', user_xids: '' }, userList: { name: '', user_xids: '' },
errorMessage: '', errorMessage: [],
}); });
import Vue from 'vue';
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import Api from 'ee/api';
import waitForPromises from 'helpers/wait_for_promises';
import { userList } from '../../feature_flags/mock_data';
import createStore from 'ee/user_lists/store/edit';
import EditUserList from 'ee/user_lists/components/edit_user_list.vue';
import UserListForm from 'ee/user_lists/components/user_list_form.vue';
jest.mock('ee/api');
jest.mock('~/lib/utils/url_utility');
const localVue = createLocalVue(Vue);
localVue.use(Vuex);
describe('ee/user_lists/components/edit_user_list', () => {
let wrapper;
const setInputValue = value => wrapper.find('[data-testid="user-list-name"]').setValue(value);
const click = button => wrapper.find(`[data-testid="${button}"]`).trigger('click');
const clickSave = () => click('save-user-list');
const destroy = () => wrapper?.destroy();
const factory = () => {
destroy();
wrapper = mount(EditUserList, {
localVue,
store: createStore({ projectId: '1', userListIid: '2' }),
propsData: {
userListsDocsPath: '/docs/user_lists',
},
});
};
afterEach(() => {
destroy();
});
describe('loading', () => {
beforeEach(() => {
Api.fetchFeatureFlagUserList.mockReturnValue(new Promise(() => {}));
factory();
});
it('should show a loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
describe('loading error', () => {
const message = 'error creating list';
let alert;
beforeEach(async () => {
Api.fetchFeatureFlagUserList.mockRejectedValue({ message });
factory();
await waitForPromises();
alert = wrapper.find(GlAlert);
});
it('should show a flash with the error respopnse', () => {
expect(alert.text()).toContain(message);
});
it('should not be dismissible', async () => {
expect(alert.props('dismissible')).toBe(false);
});
it('should not show a user list form', () => {
expect(wrapper.find(UserListForm).exists()).toBe(false);
});
});
describe('update', () => {
beforeEach(() => {
Api.fetchFeatureFlagUserList.mockResolvedValue({ data: userList });
factory();
return wrapper.vm.$nextTick();
});
it('should link to the documentation', () => {
const link = wrapper.find('[data-testid="user-list-docs-link"]');
expect(link.attributes('href')).toBe('/docs/user_lists');
});
it('should link the cancel button to the user list details path', () => {
const link = wrapper.find('[data-testid="user-list-cancel"]');
expect(link.attributes('href')).toBe(userList.path);
});
it('should show the user list name in the title', () => {
expect(wrapper.find('[data-testid="user-list-title"]').text()).toBe(`Edit ${userList.name}`);
});
describe('success', () => {
beforeEach(() => {
Api.updateFeatureFlagUserList.mockResolvedValue({ data: userList });
setInputValue('test');
clickSave();
return wrapper.vm.$nextTick();
});
it('should create a user list with the entered name', () => {
expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', {
name: 'test',
});
});
it('should redirect to the feature flag details page', () => {
expect(redirectTo).toHaveBeenCalledWith(userList.path);
});
});
describe('error', () => {
let alert;
let message;
beforeEach(async () => {
message = 'error creating list';
Api.updateFeatureFlagUserList.mockRejectedValue({ message });
setInputValue('test');
clickSave();
await waitForPromises();
alert = wrapper.find(GlAlert);
});
it('should show a flash with the error respopnse', () => {
expect(alert.text()).toContain(message);
});
it('should dismiss the error if dismiss is clicked', async () => {
alert.find('button').trigger('click');
await wrapper.vm.$nextTick();
expect(alert.exists()).toBe(false);
});
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import Api from 'ee/api';
import createStore from 'ee/user_lists/store/new';
import NewUserList from 'ee/user_lists/components/new_user_list.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { userList } from '../../feature_flags/mock_data';
jest.mock('ee/api');
jest.mock('~/lib/utils/url_utility');
const localVue = createLocalVue(Vue);
localVue.use(Vuex);
describe('ee/user_lists/components/new_user_list', () => {
let wrapper;
const setInputValue = value => wrapper.find('[data-testid="user-list-name"]').setValue(value);
const click = button => wrapper.find(`[data-testid="${button}"]`).trigger('click');
beforeEach(() => {
wrapper = mount(NewUserList, {
localVue,
store: createStore({ projectId: '1' }),
propsData: {
featureFlagsPath: '/feature_flags',
userListsDocsPath: '/docs/user_lists',
},
});
});
it('should link to the documentation', () => {
const link = wrapper.find('[data-testid="user-list-docs-link"]');
expect(link.attributes('href')).toBe('/docs/user_lists');
});
it('should link the cancel buton back to feature flags', () => {
const cancel = wrapper.find('[data-testid="user-list-cancel"');
expect(cancel.attributes('href')).toBe('/feature_flags');
});
describe('create', () => {
describe('success', () => {
beforeEach(() => {
Api.createFeatureFlagUserList.mockResolvedValue({ data: userList });
setInputValue('test');
click('save-user-list');
return wrapper.vm.$nextTick();
});
it('should create a user list with the entered name', () => {
expect(Api.createFeatureFlagUserList).toHaveBeenCalledWith('1', {
name: 'test',
user_xids: '',
});
});
it('should redirect to the feature flag details page', () => {
expect(redirectTo).toHaveBeenCalledWith(userList.path);
});
});
describe('error', () => {
let alert;
beforeEach(async () => {
Api.createFeatureFlagUserList.mockRejectedValue({ message: 'error creating list' });
setInputValue('test');
click('save-user-list');
await waitForPromises();
alert = wrapper.find(GlAlert);
});
it('should show a flash with the error respopnse', () => {
expect(alert.text()).toContain('error creating list');
});
it('should dismiss the error when the dismiss button is clicked', async () => {
alert.find('button').trigger('click');
await wrapper.vm.$nextTick();
expect(alert.exists()).toBe(false);
});
});
});
});
import { mount } from '@vue/test-utils';
import { userList } from '../../feature_flags/mock_data';
import Form from 'ee/user_lists/components/user_list_form.vue';
describe('ee/user_lists/components/user_list_form', () => {
let wrapper;
let input;
beforeEach(() => {
wrapper = mount(Form, {
propsData: {
cancelPath: '/cancel',
saveButtonLabel: 'Save',
userListsDocsPath: '/docs',
userList,
},
});
input = wrapper.find('[data-testid="user-list-name"]');
});
it('should set the name to the name of the given user list', () => {
expect(input.element.value).toBe(userList.name);
});
it('should link to the user lists docs', () => {
expect(wrapper.find('[data-testid="user-list-docs-link"]').attributes('href')).toBe('/docs');
});
it('should emit an updated user list when save is clicked', () => {
input.setValue('test');
wrapper.find('[data-testid="save-user-list"]').trigger('click');
expect(wrapper.emitted('submit')).toEqual([[{ ...userList, name: 'test' }]]);
});
it('should set the cancel button to the passed url', () => {
expect(wrapper.find('[data-testid="user-list-cancel"]').attributes('href')).toBe('/cancel');
});
});
...@@ -85,7 +85,9 @@ describe('User Lists Edit Actions', () => { ...@@ -85,7 +85,9 @@ describe('User Lists Edit Actions', () => {
it('should commit RECEIVE_USER_LIST_SUCCESS', () => { it('should commit RECEIVE_USER_LIST_SUCCESS', () => {
return testAction(actions.updateUserList, updatedList, state, [], [], () => { return testAction(actions.updateUserList, updatedList, state, [], [], () => {
expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', updatedList); expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', {
name: updatedList.name,
});
expect(redirectTo).toHaveBeenCalledWith(userList.path); expect(redirectTo).toHaveBeenCalledWith(userList.path);
}); });
}); });
...@@ -106,7 +108,10 @@ describe('User Lists Edit Actions', () => { ...@@ -106,7 +108,10 @@ describe('User Lists Edit Actions', () => {
state, state,
[{ type: types.RECEIVE_USER_LIST_ERROR, payload: ['error'] }], [{ type: types.RECEIVE_USER_LIST_ERROR, payload: ['error'] }],
[], [],
() => expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', updatedList), () =>
expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', {
name: updatedList.name,
}),
); );
}); });
}); });
......
...@@ -25959,12 +25959,33 @@ msgstr "" ...@@ -25959,12 +25959,33 @@ msgstr ""
msgid "UserLists|Cancel" msgid "UserLists|Cancel"
msgstr "" msgstr ""
msgid "UserLists|Create"
msgstr ""
msgid "UserLists|Define a set of users to be used within feature flag strategies" msgid "UserLists|Define a set of users to be used within feature flag strategies"
msgstr "" msgstr ""
msgid "UserLists|Edit %{name}"
msgstr ""
msgid "UserLists|Enter a comma separated list of user IDs. These IDs should be the users of the system in which the feature flag is set, not GitLab IDs" msgid "UserLists|Enter a comma separated list of user IDs. These IDs should be the users of the system in which the feature flag is set, not GitLab IDs"
msgstr "" msgstr ""
msgid "UserLists|Feature flag list"
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|Name"
msgstr ""
msgid "UserLists|New list"
msgstr ""
msgid "UserLists|Save"
msgstr ""
msgid "UserLists|There are no users" msgid "UserLists|There are no users"
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