Commit 108a1a40 authored by Simon Knox's avatar Simon Knox

Merge branch '334812-create-a-new-feature' into 'master'

Resolve "Create a new Feature"

See merge request gitlab-org/gitlab!73275
parents 7c609212 0790029f
#import './widget.fragment.graphql'
mutation createWorkItem($input: CreateWorkItemInput) {
createWorkItem(input: $input) @client {
workItem {
id
type
widgets {
nodes {
...WidgetBase
... on TitleWidget {
contentText
}
}
}
}
}
}
...@@ -4,6 +4,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; ...@@ -4,6 +4,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import workItemQuery from './work_item.query.graphql'; import workItemQuery from './work_item.query.graphql';
import introspectionQueryResultData from './fragmentTypes.json'; import introspectionQueryResultData from './fragmentTypes.json';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql'; import typeDefs from './typedefs.graphql';
const fragmentMatcher = new IntrospectionFragmentMatcher({ const fragmentMatcher = new IntrospectionFragmentMatcher({
...@@ -13,15 +14,12 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ ...@@ -13,15 +14,12 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
export function createApolloProvider() { export function createApolloProvider() {
Vue.use(VueApollo); Vue.use(VueApollo);
const defaultClient = createDefaultClient( const defaultClient = createDefaultClient(resolvers, {
{},
{
cacheConfig: { cacheConfig: {
fragmentMatcher, fragmentMatcher,
}, },
typeDefs, typeDefs,
}, });
);
defaultClient.cache.writeQuery({ defaultClient.cache.writeQuery({
query: workItemQuery, query: workItemQuery,
......
import { uuids } from '~/lib/utils/uuids';
import workItemQuery from './work_item.query.graphql';
export const resolvers = {
Mutation: {
createWorkItem(_, { input }, { cache }) {
const id = uuids()[0];
const workItem = {
__typename: 'WorkItem',
type: 'FEATURE',
id,
widgets: {
__typename: 'WorkItemWidgetConnection',
nodes: [
{
__typename: 'TitleWidget',
type: 'TITLE',
enabled: true,
contentText: input.title,
},
],
},
};
cache.writeQuery({ query: workItemQuery, variables: { id }, data: { workItem } });
return {
__typename: 'CreateWorkItemPayload',
workItem,
};
},
},
};
...@@ -33,6 +33,18 @@ type WorkItem { ...@@ -33,6 +33,18 @@ type WorkItem {
widgets: [WorkItemWidgetConnection] widgets: [WorkItemWidgetConnection]
} }
type CreateWorkItemInput {
title: String!
}
type CreateWorkItemPayload {
workItem: WorkItem!
}
extend type Query { extend type Query {
workItem(id: ID!): WorkItem! workItem(id: ID!): WorkItem!
} }
extend type Mutation {
createWorkItem(input: CreateWorkItemInput!): CreateWorkItemPayload!
}
<script>
import { GlButton, GlAlert } from '@gitlab/ui';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
export default {
components: {
GlButton,
GlAlert,
},
data() {
return {
title: '',
error: false,
};
},
methods: {
async createWorkItem() {
try {
const response = await this.$apollo.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
title: this.title,
},
},
});
const {
data: {
createWorkItem: {
workItem: { id },
},
},
} = response;
this.$router.push({ name: 'workItem', params: { id } });
} catch {
this.error = true;
}
},
},
};
</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>
<label for="title" class="gl-sr-only">{{ __('Title') }}</label>
<input
id="title"
v-model.trim="title"
type="text"
class="gl-font-size-h-display gl-font-weight-bold gl-my-5 gl-border-none gl-w-full gl-pl-2"
data-testid="title-input"
:placeholder="__('Add a title…')"
/>
<div class="gl-bg-gray-10 gl-py-5 gl-px-6">
<gl-button
variant="confirm"
:disabled="title.length === 0"
class="gl-mr-3"
data-testid="create-button"
type="submit"
@click="createWorkItem"
>
{{ __('Create') }}
</gl-button>
<gl-button data-testid="cancel-button" @click="$router.go(-1)">
{{ __('Cancel') }}
</gl-button>
</div>
</form>
</template>
export const routes = [ export const routes = [
{
path: '/new',
name: 'createWorkItem',
component: () => import('../pages/create_work_item.vue'),
},
{ {
path: '/:id', path: '/:id',
name: 'work_item', name: 'workItem',
component: () => import('../pages/work_item_root.vue'), component: () => import('../pages/work_item_root.vue'),
props: true, props: true,
}, },
......
...@@ -1977,6 +1977,9 @@ msgstr "" ...@@ -1977,6 +1977,9 @@ msgstr ""
msgid "Add a task list" msgid "Add a task list"
msgstr "" msgstr ""
msgid "Add a title…"
msgstr ""
msgid "Add a to do" msgid "Add a to do"
msgstr "" msgstr ""
...@@ -32373,6 +32376,9 @@ msgstr "" ...@@ -32373,6 +32376,9 @@ msgstr ""
msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}" msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
msgstr "" msgstr ""
msgid "Something went wrong when creating a work item. Please try again"
msgstr ""
msgid "Something went wrong when reordering designs. Please try again" msgid "Something went wrong when reordering designs. Please try again"
msgstr "" msgstr ""
......
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } 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 { resolvers } from '~/work_items/graphql/resolvers';
Vue.use(VueApollo);
describe('Create work item component', () => {
let wrapper;
let fakeApollo;
const findAlert = () => wrapper.findComponent(GlAlert);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findTitleInput = () => wrapper.find('[data-testid="title-input"]');
const createComponent = ({ data = {} } = {}) => {
fakeApollo = createMockApollo([], resolvers);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
return {
...data,
};
},
mocks: {
$router: {
go: jest.fn(),
push: jest.fn(),
},
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('does not render error by default', () => {
createComponent();
expect(findAlert().exists()).toBe(false);
});
it('renders a disabled Create button when title input is empty', () => {
createComponent();
expect(findCreateButton().props('disabled')).toBe(true);
});
it('redirects to the previous page on Cancel button click', () => {
createComponent();
findCancelButton().vm.$emit('click');
expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1);
});
it('hides the alert on dismissing the error', async () => {
createComponent({ data: { error: true } });
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
describe('when title input field has a text', () => {
beforeEach(() => {
createComponent();
findTitleInput().setValue('Test title');
});
it('renders a non-disabled Create button', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
it('redirects to the work item page on successful mutation', async () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(wrapper.vm.$router.push).toHaveBeenCalled();
});
// TODO: write a proper test here when we have a backend implementation
it.todo('shows an alert on mutation error');
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { workItemQueryResponse } from '../mock_data'; import { workItemQueryResponse } from '../mock_data';
const localVue = createLocalVue(); Vue.use(VueApollo);
localVue.use(VueApollo);
const WORK_ITEM_ID = '1'; const WORK_ITEM_ID = '1';
...@@ -30,7 +30,6 @@ describe('Work items root component', () => { ...@@ -30,7 +30,6 @@ describe('Work items root component', () => {
propsData: { propsData: {
id: WORK_ITEM_ID, id: WORK_ITEM_ID,
}, },
localVue,
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
}); });
}; };
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import App from '~/work_items/components/app.vue'; import App from '~/work_items/components/app.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router'; import { createRouter } from '~/work_items/router';
...@@ -27,4 +28,10 @@ describe('Work items router', () => { ...@@ -27,4 +28,10 @@ describe('Work items router', () => {
expect(wrapper.find(WorkItemsRoot).exists()).toBe(true); expect(wrapper.find(WorkItemsRoot).exists()).toBe(true);
}); });
it('renders create work item page on `/new` route', async () => {
await createComponent('/new');
expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(true);
});
}); });
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