Commit 7b4fd170 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '119320-allow-fetching-all-projects-epics-tree' into 'master'

Support async loading & search of projects

See merge request gitlab-org/gitlab!26661
parents 94f983b8 7786e2c9
<script> <script>
import { GlButton, GlDropdown, GlDropdownItem, GlFormInput } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex';
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlFormInput,
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import { SEARCH_DEBOUNCE } from '../constants';
export default { export default {
components: { components: {
...@@ -11,24 +20,20 @@ export default { ...@@ -11,24 +20,20 @@ export default {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlFormInput, GlFormInput,
GlSearchBoxByType,
GlLoadingIcon,
ProjectAvatar, ProjectAvatar,
}, },
props: {
projects: {
type: Array,
required: true,
},
},
data() { data() {
return { return {
selectedProject: null, selectedProject: null,
searchKey: '',
title: '', title: '',
preventDropdownClose: false,
}; };
}, },
computed: { computed: {
...mapState(['projectsFetchInProgress', 'itemCreateInProgress', 'projects']),
dropdownToggleText() { dropdownToggleText() {
if (this.selectedProject) { if (this.selectedProject) {
return this.selectedProject.name_with_namespace; return this.selectedProject.name_with_namespace;
...@@ -36,19 +41,37 @@ export default { ...@@ -36,19 +41,37 @@ export default {
return __('Select a project'); return __('Select a project');
}, },
},
hasValidInput() { watch: {
return this.title.trim() !== '' && this.selectedProject; /**
* We're using `debounce` here as `GlSearchBoxByType` doesn't
* support `lazy` or `debounce` props as per https://bootstrap-vue.js.org/docs/components/form-input/.
* This is a known GitLab UI issue https://gitlab.com/gitlab-org/gitlab-ui/-/issues/631
*/
searchKey: debounce(function debounceSearch() {
this.fetchProjects(this.searchKey);
}, SEARCH_DEBOUNCE),
/**
* As Issue Create Form already has `autofocus` set for
* Issue title field, we cannot leverage `autofocus` prop
* again for search input field, so we manually set
* focus only when dropdown is opened and content is loaded.
*/
projectsFetchInProgress(value) {
if (!value) {
this.$nextTick(() => {
this.$refs.searchInputField.focusInput();
});
}
}, },
}, },
methods: { methods: {
...mapActions(['fetchProjects']),
cancel() { cancel() {
this.$emit('cancel'); this.$emit('cancel');
}, },
createIssue() { createIssue() {
if (!this.hasValidInput) { if (!this.selectedProject) {
return; return;
} }
...@@ -56,6 +79,36 @@ export default { ...@@ -56,6 +79,36 @@ export default {
const { issues: issuesEndpoint } = selectedProject._links; const { issues: issuesEndpoint } = selectedProject._links;
this.$emit('submit', { issuesEndpoint, title }); this.$emit('submit', { issuesEndpoint, title });
}, },
handleDropdownShow() {
this.searchKey = '';
this.fetchProjects();
},
handleDropdownHide(e) {
// Check if dropdown closure is to be prevented.
if (this.preventDropdownClose) {
e.preventDefault();
this.preventDropdownClose = false;
}
},
/**
* As GlDropdown can get closed if any item within
* it is clicked, we have to work around that behaviour
* by preventing dropdown close if user has clicked
* clear button on search input field. This hack
* won't be required once we add support for
* `BDropdownForm` https://bootstrap-vue.js.org/docs/components/dropdown#b-dropdown-form
* within GitLab UI.
*/
handleSearchInputContainerClick({ target }) {
// Check if clicked target was an icon.
if (
target?.classList.contains('gl-icon') ||
target?.getAttribute('href')?.includes('clear')
) {
// Enable flag to prevent dropdown close.
this.preventDropdownClose = true;
}
},
}, },
}; };
</script> </script>
...@@ -67,7 +120,7 @@ export default { ...@@ -67,7 +120,7 @@ export default {
<label class="label-bold">{{ s__('Issue|Title') }}</label> <label class="label-bold">{{ s__('Issue|Title') }}</label>
<gl-form-input <gl-form-input
ref="titleInput" ref="titleInput"
v-model="title" v-model.trim="title"
:placeholder="__('New issue title')" :placeholder="__('New issue title')"
autofocus autofocus
/> />
...@@ -75,30 +128,55 @@ export default { ...@@ -75,30 +128,55 @@ export default {
<div class="col-sm"> <div class="col-sm">
<label class="label-bold">{{ __('Project') }}</label> <label class="label-bold">{{ __('Project') }}</label>
<gl-dropdown <gl-dropdown
ref="dropdownButton"
:text="dropdownToggleText" :text="dropdownToggleText"
class="w-100" class="w-100 projects-dropdown"
menu-class="w-100" menu-class="w-100 overflow-hidden"
toggle-class="d-flex align-items-center justify-content-between text-truncate" toggle-class="d-flex align-items-center justify-content-between text-truncate"
@show="handleDropdownShow"
@hide="handleDropdownHide"
> >
<gl-dropdown-item <div class="mx-2 mb-1" @click="handleSearchInputContainerClick">
v-for="project in projects" <gl-search-box-by-type
:key="project.id" ref="searchInputField"
class="w-100" v-model="searchKey"
@click="selectedProject = project" :disabled="projectsFetchInProgress"
> />
<project-avatar :project="project" :size="32" /> </div>
{{ project.name }} <gl-loading-icon
<div class="text-secondary">{{ project.namespace.name }}</div> v-show="projectsFetchInProgress"
</gl-dropdown-item> class="projects-fetch-loading align-items-center p-2"
size="md"
/>
<div v-if="!projectsFetchInProgress" class="dropdown-contents overflow-auto p-1">
<span v-if="!projects.length" class="d-block text-center p-2">{{
__('No matches found')
}}</span>
<gl-dropdown-item
v-for="project in projects"
:key="project.id"
class="w-100"
@click="selectedProject = project"
>
<project-avatar :project="project" :size="32" />
{{ project.name }}
<div class="text-secondary">{{ project.namespace.name }}</div>
</gl-dropdown-item>
</div>
</gl-dropdown> </gl-dropdown>
</div> </div>
</div> </div>
<div class="row my-1"> <div class="row my-1">
<div class="col-sm flex-sm-grow-0 mb-2 mb-sm-0"> <div class="col-sm flex-sm-grow-0 mb-2 mb-sm-0">
<gl-button class="w-100" variant="success" :disabled="!hasValidInput" @click="createIssue"> <gl-button
{{ __('Create issue') }} class="w-100"
</gl-button> variant="success"
:disabled="!selectedProject || itemCreateInProgress"
:loading="itemCreateInProgress"
@click="createIssue"
>{{ __('Create issue') }}</gl-button
>
</div> </div>
<div class="col-sm flex-sm-grow-0 ml-auto"> <div class="col-sm flex-sm-grow-0 ml-auto">
<gl-button class="w-100" @click="cancel">{{ __('Cancel') }}</gl-button> <gl-button class="w-100" @click="cancel">{{ __('Cancel') }}</gl-button>
......
...@@ -37,11 +37,6 @@ export default { ...@@ -37,11 +37,6 @@ export default {
IssueActionsSplitButton, IssueActionsSplitButton,
SlotSwitch, SlotSwitch,
}, },
data() {
return {
isCreateIssueFormVisible: false,
};
},
computed: { computed: {
...mapState([ ...mapState([
'parentItem', 'parentItem',
...@@ -54,6 +49,7 @@ export default { ...@@ -54,6 +49,7 @@ export default {
'itemCreateInProgress', 'itemCreateInProgress',
'showAddItemForm', 'showAddItemForm',
'showCreateEpicForm', 'showCreateEpicForm',
'showCreateIssueForm',
'autoCompleteEpics', 'autoCompleteEpics',
'autoCompleteIssues', 'autoCompleteIssues',
'pendingReferences', 'pendingReferences',
...@@ -61,7 +57,6 @@ export default { ...@@ -61,7 +57,6 @@ export default {
'issuableType', 'issuableType',
'epicsEndpoint', 'epicsEndpoint',
'issuesEndpoint', 'issuesEndpoint',
'projects',
]), ]),
...mapGetters(['itemAutoCompleteSources', 'itemPathIdSeparator', 'directChildren']), ...mapGetters(['itemAutoCompleteSources', 'itemPathIdSeparator', 'directChildren']),
disableContents() { disableContents() {
...@@ -76,7 +71,7 @@ export default { ...@@ -76,7 +71,7 @@ export default {
return FORM_SLOTS.createEpic; return FORM_SLOTS.createEpic;
} }
if (this.isCreateIssueFormVisible) { if (this.showCreateIssueForm) {
return FORM_SLOTS.createIssue; return FORM_SLOTS.createIssue;
} }
...@@ -93,6 +88,7 @@ export default { ...@@ -93,6 +88,7 @@ export default {
'fetchItems', 'fetchItems',
'toggleAddItemForm', 'toggleAddItemForm',
'toggleCreateEpicForm', 'toggleCreateEpicForm',
'toggleCreateIssueForm',
'setPendingReferences', 'setPendingReferences',
'addPendingReferences', 'addPendingReferences',
'removePendingReference', 'removePendingReference',
...@@ -137,15 +133,11 @@ export default { ...@@ -137,15 +133,11 @@ export default {
this.toggleCreateEpicForm({ toggleState: false }); this.toggleCreateEpicForm({ toggleState: false });
this.setItemInputValue(''); this.setItemInputValue('');
}, },
showAddIssueForm() { handleShowAddIssueForm() {
this.toggleAddItemForm({ toggleState: true, issuableType: issuableTypesMap.ISSUE }); this.toggleAddItemForm({ toggleState: true, issuableType: issuableTypesMap.ISSUE });
}, },
showCreateIssueForm() { handleShowCreateIssueForm() {
return this.fetchProjects().then(() => { this.toggleCreateIssueForm({ toggleState: true });
this.toggleAddItemForm({ toggleState: false });
this.toggleCreateEpicForm({ toggleState: false });
this.isCreateIssueFormVisible = true;
});
}, },
}, },
}; };
...@@ -168,8 +160,8 @@ export default { ...@@ -168,8 +160,8 @@ export default {
<issue-actions-split-button <issue-actions-split-button
slot="issueActions" slot="issueActions"
class="ml-1" class="ml-1"
@showAddIssueForm="showAddIssueForm" @showAddIssueForm="handleShowAddIssueForm"
@showCreateIssueForm="showCreateIssueForm" @showCreateIssueForm="handleShowCreateIssueForm"
/> />
</related-items-tree-header> </related-items-tree-header>
<slot-switch <slot-switch
...@@ -206,8 +198,7 @@ export default { ...@@ -206,8 +198,7 @@ export default {
/> />
<create-issue-form <create-issue-form
:slot="$options.FORM_SLOTS.createIssue" :slot="$options.FORM_SLOTS.createIssue"
:projects="projects" @cancel="toggleCreateIssueForm({ toggleState: false })"
@cancel="isCreateIssueFormVisible = false"
@submit="createNewIssue" @submit="createNewIssue"
/> />
</slot-switch> </slot-switch>
......
...@@ -39,4 +39,6 @@ export const RemoveItemModalProps = { ...@@ -39,4 +39,6 @@ export const RemoveItemModalProps = {
export const OVERFLOW_AFTER = 5; export const OVERFLOW_AFTER = 5;
export const SEARCH_DEBOUNCE = 500;
export const itemRemoveModalId = 'item-remove-confirmation'; export const itemRemoveModalId = 'item-remove-confirmation';
...@@ -252,6 +252,8 @@ export const removeItem = ({ dispatch }, { parentItem, item }) => { ...@@ -252,6 +252,8 @@ export const removeItem = ({ dispatch }, { parentItem, item }) => {
export const toggleAddItemForm = ({ commit }, data) => commit(types.TOGGLE_ADD_ITEM_FORM, data); export const toggleAddItemForm = ({ commit }, data) => commit(types.TOGGLE_ADD_ITEM_FORM, data);
export const toggleCreateEpicForm = ({ commit }, data) => export const toggleCreateEpicForm = ({ commit }, data) =>
commit(types.TOGGLE_CREATE_EPIC_FORM, data); commit(types.TOGGLE_CREATE_EPIC_FORM, data);
export const toggleCreateIssueForm = ({ commit }, data) =>
commit(types.TOGGLE_CREATE_ISSUE_FORM, data);
export const setPendingReferences = ({ commit }, data) => export const setPendingReferences = ({ commit }, data) =>
commit(types.SET_PENDING_REFERENCES, data); commit(types.SET_PENDING_REFERENCES, data);
...@@ -448,42 +450,62 @@ export const reorderItem = ( ...@@ -448,42 +450,62 @@ export const reorderItem = (
}); });
}; };
export const receiveCreateIssueSuccess = ({ commit }) =>
commit(types.RECEIVE_CREATE_ITEM_SUCCESS, { insertAt: 0, items: [] });
export const receiveCreateIssueFailure = ({ commit }) => {
commit(types.RECEIVE_CREATE_ITEM_FAILURE);
flash(s__('Epics|Something went wrong while creating issue.'));
};
export const createNewIssue = ({ state, dispatch }, { issuesEndpoint, title }) => { export const createNewIssue = ({ state, dispatch }, { issuesEndpoint, title }) => {
const { parentItem } = state; const { parentItem } = state;
// necessary because parentItem comes from GraphQL and we are using REST API here // necessary because parentItem comes from GraphQL and we are using REST API here
const epicId = parseInt(parentItem.id.replace(/^gid:\/\/gitlab\/Epic\//, ''), 10); const epicId = parseInt(parentItem.id.replace(/^gid:\/\/gitlab\/Epic\//, ''), 10);
dispatch('requestCreateItem');
return axios return axios
.post(issuesEndpoint, { epic_id: epicId, title }) .post(issuesEndpoint, { epic_id: epicId, title })
.then(() => .then(({ data }) => {
dispatch('receiveCreateIssueSuccess', data);
dispatch('fetchItems', { dispatch('fetchItems', {
parentItem, parentItem,
}), });
) })
.catch(e => { .catch(e => {
flash(__('Could not create issue')); dispatch('receiveCreateIssueFailure');
throw e; throw e;
}); });
}; };
export const fetchProjects = ({ state, commit }) => export const requestProjects = ({ commit }) => commit(types.REQUEST_PROJECTS);
export const receiveProjectsSuccess = ({ commit }, data) =>
commit(types.RECIEVE_PROJECTS_SUCCESS, data);
export const receiveProjectsFailure = ({ commit }) => {
commit(types.RECIEVE_PROJECTS_FAILURE);
flash(__('Something went wrong while fetching projects.'));
};
export const fetchProjects = ({ state, dispatch }, searchKey = '') => {
const params = {
include_subgroups: true,
order_by: 'last_activity_at',
with_issues_enabled: true,
with_shared: false,
};
if (searchKey) {
params.search = searchKey;
}
dispatch('requestProjects');
axios axios
.get(state.projectsEndpoint, { .get(state.projectsEndpoint, {
params: { params,
include_subgroups: true,
order_by: 'last_activity_at',
with_issues_enabled: true,
with_shared: false,
},
}) })
.then(({ data }) => { .then(({ data }) => {
commit(types.SET_PROJECTS, data); dispatch('receiveProjectsSuccess', data);
}) })
.catch(e => { .catch(() => dispatch('receiveProjectsFailure'));
flash(__('Could not fetch projects')); };
throw e;
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -22,6 +22,7 @@ export const COLLAPSE_ITEM = 'COLLAPSE_ITEM'; ...@@ -22,6 +22,7 @@ export const COLLAPSE_ITEM = 'COLLAPSE_ITEM';
export const TOGGLE_ADD_ITEM_FORM = 'TOGGLE_ADD_ITEM_FORM'; export const TOGGLE_ADD_ITEM_FORM = 'TOGGLE_ADD_ITEM_FORM';
export const TOGGLE_CREATE_EPIC_FORM = 'TOGGLE_CREATE_EPIC_FORM'; export const TOGGLE_CREATE_EPIC_FORM = 'TOGGLE_CREATE_EPIC_FORM';
export const TOGGLE_CREATE_ISSUE_FORM = 'TOGGLE_CREATE_ISSUE_FORM';
export const SET_PENDING_REFERENCES = 'SET_PENDING_REFERENCES'; export const SET_PENDING_REFERENCES = 'SET_PENDING_REFERENCES';
export const ADD_PENDING_REFERENCES = 'ADD_PENDING_REFERENCES'; export const ADD_PENDING_REFERENCES = 'ADD_PENDING_REFERENCES';
...@@ -40,3 +41,6 @@ export const RECEIVE_CREATE_ITEM_FAILURE = 'RECEIVE_CREATE_ITEM_FAILURE'; ...@@ -40,3 +41,6 @@ export const RECEIVE_CREATE_ITEM_FAILURE = 'RECEIVE_CREATE_ITEM_FAILURE';
export const REORDER_ITEM = 'REORDER_ITEM'; export const REORDER_ITEM = 'REORDER_ITEM';
export const SET_PROJECTS = 'SET_PROJECTS'; export const SET_PROJECTS = 'SET_PROJECTS';
export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
export const RECIEVE_PROJECTS_SUCCESS = 'RECIEVE_PROJECTS_SUCCESS';
export const RECIEVE_PROJECTS_FAILURE = 'RECIEVE_PROJECTS_FAILURE';
...@@ -144,6 +144,11 @@ export default { ...@@ -144,6 +144,11 @@ export default {
state.showAddItemForm = false; state.showAddItemForm = false;
}, },
[types.TOGGLE_CREATE_ISSUE_FORM](state, { toggleState }) {
state.showCreateIssueForm = toggleState;
state.showAddItemForm = false;
},
[types.SET_PENDING_REFERENCES](state, references) { [types.SET_PENDING_REFERENCES](state, references) {
state.pendingReferences = references; state.pendingReferences = references;
}, },
...@@ -203,7 +208,14 @@ export default { ...@@ -203,7 +208,14 @@ export default {
state.children[parentItem.reference].splice(newIndex, 0, targetItem); state.children[parentItem.reference].splice(newIndex, 0, targetItem);
}, },
[types.SET_PROJECTS](state, projects) { [types.REQUEST_PROJECTS](state) {
state.projectsFetchInProgress = true;
},
[types.RECIEVE_PROJECTS_SUCCESS](state, projects) {
state.projects = projects; state.projects = projects;
state.projectsFetchInProgress = false;
},
[types.RECIEVE_PROJECTS_FAILURE](state) {
state.projectsFetchInProgress = false;
}, },
}; };
...@@ -32,8 +32,10 @@ export default () => ({ ...@@ -32,8 +32,10 @@ export default () => ({
itemAddInProgress: false, itemAddInProgress: false,
itemAddFailure: false, itemAddFailure: false,
itemCreateInProgress: false, itemCreateInProgress: false,
projectsFetchInProgress: false,
showAddItemForm: false, showAddItemForm: false,
showCreateEpicForm: false, showCreateEpicForm: false,
showCreateIssueForm: false,
autoCompleteEpics: false, autoCompleteEpics: false,
autoCompleteIssues: false, autoCompleteIssues: false,
allowSubEpics: false, allowSubEpics: false,
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
.add-item-form-container { .add-item-form-container {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.projects-dropdown .dropdown-contents {
max-height: $dropdown-max-height - 50;
}
} }
.sub-tree-root { .sub-tree-root {
......
---
title: Support async loading & search of projects within Epics Tree
merge_request: 26661
author:
type: added
...@@ -17,6 +17,8 @@ import { getJSONFixture } from 'helpers/fixtures'; ...@@ -17,6 +17,8 @@ import { getJSONFixture } from 'helpers/fixtures';
import { import {
mockInitialConfig, mockInitialConfig,
mockParentItem, mockParentItem,
mockEpics,
mockIssues,
} from '../../../javascripts/related_items_tree/mock_data'; } from '../../../javascripts/related_items_tree/mock_data';
const mockProjects = getJSONFixture('static/projects.json'); const mockProjects = getJSONFixture('static/projects.json');
...@@ -26,6 +28,10 @@ const createComponent = () => { ...@@ -26,6 +28,10 @@ const createComponent = () => {
store.dispatch('setInitialConfig', mockInitialConfig); store.dispatch('setInitialConfig', mockInitialConfig);
store.dispatch('setInitialParentItem', mockParentItem); store.dispatch('setInitialParentItem', mockParentItem);
store.dispatch('setItemChildren', {
parentItem: mockParentItem,
children: [...mockEpics, ...mockIssues],
});
return shallowMount(RelatedItemsTreeApp, { return shallowMount(RelatedItemsTreeApp, {
store, store,
...@@ -41,7 +47,6 @@ describe('RelatedItemsTreeApp', () => { ...@@ -41,7 +47,6 @@ describe('RelatedItemsTreeApp', () => {
const findIssueActionsSplitButton = () => wrapper.find(IssueActionsSplitButton); const findIssueActionsSplitButton = () => wrapper.find(IssueActionsSplitButton);
const showCreateIssueForm = () => { const showCreateIssueForm = () => {
findIssueActionsSplitButton().vm.$emit('showCreateIssueForm'); findIssueActionsSplitButton().vm.$emit('showCreateIssueForm');
return axios.waitFor(mockInitialConfig.projectsEndpoint).then(() => wrapper.vm.$nextTick());
}; };
beforeEach(() => { beforeEach(() => {
...@@ -273,11 +278,10 @@ describe('RelatedItemsTreeApp', () => { ...@@ -273,11 +278,10 @@ describe('RelatedItemsTreeApp', () => {
it('shows create item form', () => { it('shows create item form', () => {
expect(findCreateIssueForm().exists()).toBe(false); expect(findCreateIssueForm().exists()).toBe(false);
return showCreateIssueForm().then(() => { showCreateIssueForm();
const form = findCreateIssueForm();
expect(form.exists()).toBe(true); return wrapper.vm.$nextTick(() => {
expect(form.props().projects).toBe(mockProjects); expect(findCreateIssueForm().exists()).toBe(true);
}); });
}); });
}); });
......
...@@ -392,6 +392,17 @@ describe('RelatedItemsTree', () => { ...@@ -392,6 +392,17 @@ describe('RelatedItemsTree', () => {
}); });
}); });
describe(types.TOGGLE_CREATE_ISSUE_FORM, () => {
it('should set value of `showCreateIssueForm` as it is and `showAddItemForm` as false on state', () => {
const data = { toggleState: true };
mutations[types.TOGGLE_CREATE_ISSUE_FORM](state, data);
expect(state.showCreateIssueForm).toBe(data.toggleState);
expect(state.showAddItemForm).toBe(false);
});
});
describe(types.SET_PENDING_REFERENCES, () => { describe(types.SET_PENDING_REFERENCES, () => {
it('should set `pendingReferences` to state based on provided `references` param', () => { it('should set `pendingReferences` to state based on provided `references` param', () => {
const reference = ['foo']; const reference = ['foo'];
...@@ -546,6 +557,33 @@ describe('RelatedItemsTree', () => { ...@@ -546,6 +557,33 @@ describe('RelatedItemsTree', () => {
); );
}); });
}); });
describe(types.REQUEST_PROJECTS, () => {
it('should set `projectsFetchInProgress` to true within state', () => {
mutations[types.REQUEST_PROJECTS](state);
expect(state.projectsFetchInProgress).toBe(true);
});
});
describe(types.RECIEVE_PROJECTS_SUCCESS, () => {
it('should set `projectsFetchInProgress` to false and provided `projects` param as it is within the state', () => {
const projects = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }];
mutations[types.RECIEVE_PROJECTS_SUCCESS](state, projects);
expect(state.projects).toBe(projects);
expect(state.projectsFetchInProgress).toBe(false);
});
});
describe(types.RECIEVE_PROJECTS_FAILURE, () => {
it('should set `projectsFetchInProgress` to false within state', () => {
mutations[types.RECIEVE_PROJECTS_FAILURE](state);
expect(state.projectsFetchInProgress).toBe(false);
});
});
}); });
}); });
}); });
...@@ -27,6 +27,8 @@ import { ...@@ -27,6 +27,8 @@ import {
mockEpic1, mockEpic1,
} from '../mock_data'; } from '../mock_data';
const mockProjects = getJSONFixture('static/projects.json');
describe('RelatedItemTree', () => { describe('RelatedItemTree', () => {
describe('store', () => { describe('store', () => {
describe('actions', () => { describe('actions', () => {
...@@ -801,6 +803,19 @@ describe('RelatedItemTree', () => { ...@@ -801,6 +803,19 @@ describe('RelatedItemTree', () => {
}); });
}); });
describe('toggleCreateIssueForm', () => {
it('should set `state.showCreateIssueForm` to true and `state.showAddItemForm` to false', done => {
testAction(
actions.toggleCreateIssueForm,
{},
{},
[{ type: types.TOGGLE_CREATE_ISSUE_FORM, payload: {} }],
[],
done,
);
});
});
describe('setPendingReferences', () => { describe('setPendingReferences', () => {
it('should set param value to `state.pendingReference`', done => { it('should set param value to `state.pendingReference`', done => {
testAction( testAction(
...@@ -1325,6 +1340,52 @@ describe('RelatedItemTree', () => { ...@@ -1325,6 +1340,52 @@ describe('RelatedItemTree', () => {
}); });
}); });
describe('receiveCreateIssueSuccess', () => {
it('should set `state.itemCreateInProgress` & `state.itemsFetchResultEmpty` to false', done => {
testAction(
actions.receiveCreateIssueSuccess,
{ insertAt: 0, items: [] },
{},
[{ type: types.RECEIVE_CREATE_ITEM_SUCCESS, payload: { insertAt: 0, items: [] } }],
[],
done,
);
});
});
describe('receiveCreateIssueFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('should set `state.itemCreateInProgress` to false', done => {
testAction(
actions.receiveCreateIssueFailure,
{},
{},
[{ type: types.RECEIVE_CREATE_ITEM_FAILURE }],
[],
done,
);
});
it('should show flash error with message "Something went wrong while creating issue."', () => {
const message = 'Something went wrong while creating issue.';
actions.receiveCreateIssueFailure(
{
commit: () => {},
},
{
message,
},
);
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
message,
);
});
});
describe('createNewIssue', () => { describe('createNewIssue', () => {
const issuesEndpoint = `${TEST_HOST}/issues`; const issuesEndpoint = `${TEST_HOST}/issues`;
const title = 'new issue title'; const title = 'new issue title';
...@@ -1382,6 +1443,8 @@ describe('RelatedItemTree', () => { ...@@ -1382,6 +1443,8 @@ describe('RelatedItemTree', () => {
.createNewIssue(context, payload) .createNewIssue(context, payload)
.then(() => { .then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest); expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
expect(context.dispatch).toHaveBeenCalledWith('requestCreateItem');
expect(context.dispatch).toHaveBeenCalledWith('receiveCreateIssueSuccess', '');
expect(context.dispatch).toHaveBeenCalledWith( expect(context.dispatch).toHaveBeenCalledWith(
'fetchItems', 'fetchItems',
jasmine.objectContaining({ parentItem }), jasmine.objectContaining({ parentItem }),
...@@ -1405,14 +1468,120 @@ describe('RelatedItemTree', () => { ...@@ -1405,14 +1468,120 @@ describe('RelatedItemTree', () => {
.then(() => done.fail('expected action to throw error!')) .then(() => done.fail('expected action to throw error!'))
.catch(() => { .catch(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest); expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
expect(context.dispatch).not.toHaveBeenCalled(); expect(context.dispatch).toHaveBeenCalledWith('receiveCreateIssueFailure');
expect(flashSpy).toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
}); });
}); });
describe('requestProjects', () => {
it('should set `state.projectsFetchInProgress` to true', done => {
testAction(actions.requestProjects, {}, {}, [{ type: types.REQUEST_PROJECTS }], [], done);
});
});
describe('receiveProjectsSuccess', () => {
it('should set `state.projectsFetchInProgress` to false and set provided `projects` param to state', done => {
testAction(
actions.receiveProjectsSuccess,
mockProjects,
{},
[{ type: types.RECIEVE_PROJECTS_SUCCESS, payload: mockProjects }],
[],
done,
);
});
});
describe('receiveProjectsFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('should set `state.projectsFetchInProgress` to false', done => {
testAction(
actions.receiveProjectsFailure,
{},
{},
[{ type: types.RECIEVE_PROJECTS_FAILURE }],
[],
done,
);
});
it('should show flash error with message "Something went wrong while fetching projects."', () => {
const message = 'Something went wrong while fetching projects.';
actions.receiveProjectsFailure(
{
commit: () => {},
},
{
message,
},
);
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
message,
);
});
});
describe('fetchProjects', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.parentItem = mockParentItem;
state.issuableType = issuableTypesMap.EPIC;
});
afterEach(() => {
mock.restore();
});
it('should dispatch `requestProjects` and `receiveProjectsSuccess` actions on request success', done => {
mock.onGet(/(.*)/).replyOnce(200, mockProjects);
testAction(
actions.fetchProjects,
'',
state,
[],
[
{
type: 'requestProjects',
},
{
type: 'receiveProjectsSuccess',
payload: mockProjects,
},
],
done,
);
});
it('should dispatch `requestProjects` and `receiveProjectsFailure` actions on request failure', done => {
mock.onGet(/(.*)/).replyOnce(500, {});
testAction(
actions.fetchProjects,
'',
state,
[],
[
{
type: 'requestProjects',
},
{
type: 'receiveProjectsFailure',
},
],
done,
);
});
});
}); });
}); });
}); });
...@@ -5633,9 +5633,6 @@ msgstr "" ...@@ -5633,9 +5633,6 @@ msgstr ""
msgid "Could not create group" msgid "Could not create group"
msgstr "" msgstr ""
msgid "Could not create issue"
msgstr ""
msgid "Could not create project" msgid "Could not create project"
msgstr "" msgstr ""
...@@ -5645,9 +5642,6 @@ msgstr "" ...@@ -5645,9 +5642,6 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}." msgid "Could not delete chat nickname %{chat_name}."
msgstr "" msgstr ""
msgid "Could not fetch projects"
msgstr ""
msgid "Could not find design" msgid "Could not find design"
msgstr "" msgstr ""
...@@ -7843,6 +7837,9 @@ msgstr "" ...@@ -7843,6 +7837,9 @@ msgstr ""
msgid "Epics|Something went wrong while creating child epics." msgid "Epics|Something went wrong while creating child epics."
msgstr "" msgstr ""
msgid "Epics|Something went wrong while creating issue."
msgstr ""
msgid "Epics|Something went wrong while fetching child epics." msgid "Epics|Something went wrong while fetching child epics."
msgstr "" msgstr ""
...@@ -13181,6 +13178,9 @@ msgstr "" ...@@ -13181,6 +13178,9 @@ msgstr ""
msgid "No licenses found." msgid "No licenses found."
msgstr "" msgstr ""
msgid "No matches found"
msgstr ""
msgid "No matching labels" msgid "No matching labels"
msgstr "" msgstr ""
...@@ -18323,6 +18323,9 @@ msgstr "" ...@@ -18323,6 +18323,9 @@ msgstr ""
msgid "Something went wrong while fetching projects" msgid "Something went wrong while fetching projects"
msgstr "" msgstr ""
msgid "Something went wrong while fetching projects."
msgstr ""
msgid "Something went wrong while fetching related merge requests." msgid "Something went wrong while fetching related merge requests."
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