Commit f18a62ef authored by Paul Slaughter's avatar Paul Slaughter

Merge branch 'user-lists-edit-create-components' into 'master'

Add Components for Editing and Creating User Lists

See merge request gitlab-org/gitlab!37012
parents 37ce8e98 10e90560
<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
export const updateUserList = ({ commit, state }, userList) => {
return Api.updateFeatureFlagUserList(state.projectId, {
...state.userList,
...userList,
name: userList.name,
})
.then(({ data }) => redirectTo(data.path))
.catch(response => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response)));
......
......@@ -5,5 +5,5 @@ export default ({ projectId = '', userListIid = '' }) => ({
projectId,
userListIid,
userList: null,
errorMessage: '',
errorMessage: [],
});
export default ({ projectId = '' }) => ({
projectId,
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', () => {
it('should commit RECEIVE_USER_LIST_SUCCESS', () => {
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);
});
});
......@@ -106,7 +108,10 @@ describe('User Lists Edit Actions', () => {
state,
[{ type: types.RECEIVE_USER_LIST_ERROR, payload: ['error'] }],
[],
() => expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', updatedList),
() =>
expect(Api.updateFeatureFlagUserList).toHaveBeenCalledWith('1', {
name: updatedList.name,
}),
);
});
});
......
......@@ -25989,12 +25989,33 @@ msgstr ""
msgid "UserLists|Cancel"
msgstr ""
msgid "UserLists|Create"
msgstr ""
msgid "UserLists|Define a set of users to be used within feature flag strategies"
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"
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"
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