Commit 87556be9 authored by Simon Knox's avatar Simon Knox

Merge branch 'ntepluhina-create-task-followup' into 'master'

Add dropdown with work item types to create task component

See merge request gitlab-org/gitlab!80076
parents 179e55f0 b19091f3
......@@ -95,7 +95,7 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
if (this.workItemsEnabled) {
if (this.workItemsEnabled && this.$el) {
this.renderTaskActions();
}
},
......@@ -174,8 +174,11 @@ export default {
);
button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id);
button.innerHTML =
'<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"><use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#ellipsis_v"></use></svg>';
button.innerHTML = `
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
<use href="${gon.sprite_icons}#ellipsis_v"></use>
</svg>
`;
item.prepend(button);
});
},
......@@ -191,7 +194,7 @@ export default {
const taskBadge = document.createElement('span');
taskBadge.innerHTML = `
<svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12">
<use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#issue-open-m"></use>
<use href="${gon.sprite_icons}#issue-open-m"></use>
</svg>
<span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
${__('Task')}
......@@ -245,7 +248,7 @@ export default {
modal-id="create-task-modal"
:title="s__('WorkItem|New Task')"
hide-footer
body-class="gl-py-0!"
body-class="gl-p-0!"
>
<create-work-item
:is-modal="true"
......
......@@ -74,6 +74,8 @@ export function initIssueApp(issueData, store) {
return undefined;
}
const { fullPath } = el.dataset;
if (gon?.features?.fixCommentScroll) {
scrollToTargetOnResize();
}
......@@ -88,6 +90,7 @@ export function initIssueApp(issueData, store) {
store,
provide: {
canCreateIncident,
fullPath,
},
computed: {
...mapGetters(['getNoteableData']),
......
query projectWorkItemTypes($fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
id
workItemTypes {
nodes {
id
name
}
}
}
}
......@@ -5,11 +5,15 @@ import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
const { fullPath } = el.dataset;
return new Vue({
el,
router: createRouter(el.dataset.fullPath),
apolloProvider: createApolloProvider(),
provide: {
fullPath,
},
render(createElement) {
return createElement(App);
},
......
<script>
import { GlButton, GlAlert } from '@gitlab/ui';
import { GlButton, GlAlert, GlLoadingIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue';
......@@ -8,8 +10,12 @@ export default {
components: {
GlButton,
GlAlert,
GlLoadingIcon,
GlDropdown,
GlDropdownItem,
ItemTitle,
},
inject: ['fullPath'],
props: {
isModal: {
type: Boolean,
......@@ -25,9 +31,34 @@ export default {
data() {
return {
title: this.initialTitle,
error: false,
error: null,
workItemTypes: [],
selectedWorkItemType: null,
};
},
apollo: {
workItemTypes: {
query: projectWorkItemTypesQuery,
variables() {
return {
fullPath: this.fullPath,
};
},
update(data) {
return data.workspace?.workItemTypes?.nodes;
},
error() {
this.error = s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
);
},
},
},
computed: {
dropdownButtonText() {
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
},
},
methods: {
async createWorkItem() {
try {
......@@ -53,7 +84,9 @@ export default {
this.$emit('onCreate', this.title);
}
} catch {
this.error = true;
this.error = s__(
'WorkItem|Something went wrong when creating a work item. Please try again',
);
}
},
handleTitleInput(title) {
......@@ -66,18 +99,43 @@ export default {
}
this.$emit('closeModal');
},
selectWorkItemType(type) {
this.selectedWorkItemType = type;
},
},
};
</script>
<template>
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
__('Something went wrong when creating a work item. Please try again')
}}</gl-alert>
<item-title :initial-title="title" data-testid="title-input" @title-input="handleTitleInput" />
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
<div :class="{ 'gl-px-5': isModal }" data-testid="content">
<item-title
:initial-title="title"
data-testid="title-input"
@title-input="handleTitleInput"
/>
<div>
<gl-dropdown :text="dropdownButtonText">
<gl-loading-icon
v-if="$apollo.queries.workItemTypes.loading"
size="md"
data-testid="loading-types"
/>
<template v-else>
<gl-dropdown-item
v-for="type in workItemTypes"
:key="type.id"
@click="selectWorkItemType(type)"
>
{{ type.name }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
</div>
<div
class="gl-bg-gray-10 gl-py-5 gl-px-6"
class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4"
:class="{ 'gl-display-flex gl-justify-content-end': isModal }"
>
<gl-button
......
......@@ -3,7 +3,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json} }
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
.title-container
%h2.title= markdown_field(issuable, :title)
- if issuable.description.present?
......
......@@ -33812,9 +33812,6 @@ msgstr ""
msgid "Something went wrong trying to load issue contacts."
msgstr ""
msgid "Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "Something went wrong when reordering designs. Please try again"
msgstr ""
......@@ -41096,6 +41093,15 @@ msgstr ""
msgid "WorkItem|New Task"
msgstr ""
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "WorkItem|Something went wrong when fetching work item types. Please try again"
msgstr ""
msgid "WorkItem|Type"
msgstr ""
msgid "WorkItem|Work Items"
msgstr ""
......
......@@ -34,3 +34,17 @@ export const updateWorkItemMutationResponse = {
},
},
};
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
id: '1',
workItemTypes: {
nodes: [
{ id: 'work-item-1', name: 'Issue' },
{ id: 'work-item-2', name: 'Incident' },
],
},
},
},
};
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import { projectWorkItemTypesQueryResponse } from '../mock_data';
Vue.use(VueApollo);
......@@ -14,13 +16,20 @@ describe('Create work item component', () => {
let wrapper;
let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findContent = () => wrapper.find('[data-testid="content"]');
const findLoadingTypesIcon = () => wrapper.find('[data-testid="loading-types"]');
const createComponent = ({ data = {}, props = {} } = {}) => {
fakeApollo = createMockApollo([], resolvers);
const createComponent = ({ data = {}, props = {}, queryHandler = querySuccessHandler } = {}) => {
fakeApollo = createMockApollo([[projectWorkItemTypesQuery, queryHandler]], resolvers);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
......@@ -37,6 +46,9 @@ describe('Create work item component', () => {
push: jest.fn(),
},
},
provide: {
fullPath: 'full-path',
},
});
};
......@@ -84,6 +96,10 @@ describe('Create work item component', () => {
it('does not add right margin for cancel button', () => {
expect(findCancelButton().classes()).not.toContain('gl-mr-3');
});
it('does not add padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(false);
});
});
describe('when displayed in a modal', () => {
......@@ -118,6 +134,44 @@ describe('Create work item component', () => {
it('adds right margin for cancel button', () => {
expect(findCancelButton().classes()).toContain('gl-mr-3');
});
it('adds padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(true);
});
});
it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
expect(findLoadingTypesIcon().exists()).toBe(true);
});
it('displays an alert when work items query is rejected', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem') });
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain('fetching work item types');
});
describe('when work item types are fetched', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('displays a list of work item types', () => {
expect(findDropdownItems()).toHaveLength(2);
expect(findDropdownItems().at(0).text()).toContain('Issue');
});
it('selects a work item type on click', async () => {
expect(findDropdown().props('text')).toBe('Type');
findDropdownItems().at(0).vm.$emit('click');
await nextTick();
expect(findDropdown().props('text')).toBe('Issue');
});
});
it('hides the alert on dismissing the error', async () => {
......
......@@ -15,6 +15,16 @@ describe('Work items router', () => {
wrapper = mount(App, {
router,
provide: {
fullPath: 'full-path',
},
mocks: {
$apollo: {
queries: {
workItemTypes: {},
},
},
},
});
};
......
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