Commit cf42ec6d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'winh-epic-new-issue' into 'master'

Add form to create issues to epics tree feature (second attempt)

See merge request gitlab-org/gitlab!21825
parents 9f2c83be 3a5d5c7b
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlFormInput } from '@gitlab/ui';
import { __ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlFormInput,
ProjectAvatar,
},
props: {
projects: {
type: Array,
required: true,
},
},
data() {
return {
selectedProject: null,
title: '',
};
},
computed: {
dropdownToggleText() {
if (this.selectedProject) {
return this.selectedProject.name_with_namespace;
}
return __('Select a project');
},
hasValidInput() {
return this.title.trim() !== '' && this.selectedProject;
},
},
methods: {
cancel() {
this.$emit('cancel');
},
createIssue() {
if (!this.hasValidInput) {
return;
}
const { selectedProject, title } = this;
const { issues: issuesEndpoint } = selectedProject._links;
this.$emit('submit', { issuesEndpoint, title });
},
},
};
</script>
<template> <template>
<div> <div>
<!-- eslint-disable @gitlab/vue-i18n/no-bare-strings --> <div class="row mb-3">
<p> <div class="col-sm">
This is a placeholder for <label class="label-bold">{{ s__('Issue|Title') }}</label>
<a href="https://gitlab.com/gitlab-org/gitlab/issues/5419">#5419</a>. <gl-form-input
</p> ref="titleInput"
<button class="btn btn-secondary" type="button" @click="$emit('cancel')">Cancel</button> v-model="title"
:placeholder="__('New issue title')"
autofocus
/>
</div>
<div class="col-sm">
<label class="label-bold">{{ __('Project') }}</label>
<gl-dropdown
:text="dropdownToggleText"
class="w-100"
menu-class="w-100"
toggle-class="d-flex align-items-center justify-content-between text-truncate"
>
<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>
</gl-dropdown>
</div>
</div>
<div class="row my-1">
<div class="col-sm flex-sm-grow-0 mb-2 mb-sm-0">
<gl-button class="w-100" variant="success" :disabled="!hasValidInput" @click="createIssue">
{{ __('Create issue') }}
</gl-button>
</div>
<div class="col-sm flex-sm-grow-0 ml-auto">
<gl-button class="w-100" @click="cancel">{{ __('Cancel') }}</gl-button>
</div>
</div>
</div> </div>
</template> </template>
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
'issuableType', 'issuableType',
'epicsEndpoint', 'epicsEndpoint',
'issuesEndpoint', 'issuesEndpoint',
'projects',
]), ]),
...mapGetters(['itemAutoCompleteSources', 'itemPathIdSeparator', 'directChildren']), ...mapGetters(['itemAutoCompleteSources', 'itemPathIdSeparator', 'directChildren']),
disableContents() { disableContents() {
...@@ -100,6 +101,8 @@ export default { ...@@ -100,6 +101,8 @@ export default {
'setItemInputValue', 'setItemInputValue',
'addItem', 'addItem',
'createItem', 'createItem',
'createNewIssue',
'fetchProjects',
]), ]),
getRawRefs(value) { getRawRefs(value) {
return value.split(/\s+/).filter(ref => ref.trim().length > 0); return value.split(/\s+/).filter(ref => ref.trim().length > 0);
...@@ -140,9 +143,11 @@ export default { ...@@ -140,9 +143,11 @@ export default {
this.toggleAddItemForm({ toggleState: true, issuableType: issuableTypesMap.ISSUE }); this.toggleAddItemForm({ toggleState: true, issuableType: issuableTypesMap.ISSUE });
}, },
showCreateIssueForm() { showCreateIssueForm() {
this.toggleAddItemForm({ toggleState: false }); return this.fetchProjects().then(() => {
this.toggleCreateEpicForm({ toggleState: false }); this.toggleAddItemForm({ toggleState: false });
this.isCreateIssueFormVisible = true; this.toggleCreateEpicForm({ toggleState: false });
this.isCreateIssueFormVisible = true;
});
}, },
}, },
}; };
...@@ -203,7 +208,9 @@ export default { ...@@ -203,7 +208,9 @@ export default {
/> />
<create-issue-form <create-issue-form
:slot="$options.FORM_SLOTS.createIssue" :slot="$options.FORM_SLOTS.createIssue"
:projects="projects"
@cancel="isCreateIssueFormVisible = false" @cancel="isCreateIssueFormVisible = false"
@submit="createNewIssue"
/> />
</slot-switch> </slot-switch>
<related-items-tree-body <related-items-tree-body
......
...@@ -42,6 +42,7 @@ export default () => { ...@@ -42,6 +42,7 @@ export default () => {
this.setInitialConfig({ this.setInitialConfig({
epicsEndpoint: initialData.epicLinksEndpoint, epicsEndpoint: initialData.epicLinksEndpoint,
issuesEndpoint: initialData.issueLinksEndpoint, issuesEndpoint: initialData.issueLinksEndpoint,
projectsEndpoint: initialData.projectsEndpoint,
autoCompleteEpics: parseBoolean(autoCompleteEpics), autoCompleteEpics: parseBoolean(autoCompleteEpics),
autoCompleteIssues: parseBoolean(autoCompleteIssues), autoCompleteIssues: parseBoolean(autoCompleteIssues),
userSignedIn: parseBoolean(userSignedIn), userSignedIn: parseBoolean(userSignedIn),
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
relatedIssuesRemoveErrorMap, relatedIssuesRemoveErrorMap,
} from 'ee/related_issues/constants'; } from 'ee/related_issues/constants';
import flash from '~/flash'; import flash from '~/flash';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -75,7 +75,10 @@ export const receiveItemsFailure = ({ commit }, data) => { ...@@ -75,7 +75,10 @@ export const receiveItemsFailure = ({ commit }, data) => {
flash(s__('Epics|Something went wrong while fetching child epics.')); flash(s__('Epics|Something went wrong while fetching child epics.'));
commit(types.RECEIVE_ITEMS_FAILURE, data); commit(types.RECEIVE_ITEMS_FAILURE, data);
}; };
export const fetchItems = ({ dispatch }, { parentItem, isSubItem = false }) => { export const fetchItems = (
{ dispatch },
{ parentItem, isSubItem = false, fetchPolicy = 'cache-first' },
) => {
const { iid, fullPath } = parentItem; const { iid, fullPath } = parentItem;
dispatch('requestItems', { dispatch('requestItems', {
...@@ -87,6 +90,7 @@ export const fetchItems = ({ dispatch }, { parentItem, isSubItem = false }) => { ...@@ -87,6 +90,7 @@ export const fetchItems = ({ dispatch }, { parentItem, isSubItem = false }) => {
.query({ .query({
query: epicChildren, query: epicChildren,
variables: { iid, fullPath }, variables: { iid, fullPath },
fetchPolicy,
}) })
.then(({ data }) => { .then(({ data }) => {
const children = processQueryResponse(data.group); const children = processQueryResponse(data.group);
...@@ -443,5 +447,43 @@ export const reorderItem = ( ...@@ -443,5 +447,43 @@ export const reorderItem = (
}); });
}; };
export const createNewIssue = ({ state, dispatch }, { issuesEndpoint, title }) => {
const { parentItem } = state;
// necessary because parentItem comes from GraphQL and we are using REST API here
const epicId = parseInt(parentItem.id.replace(/^gid:\/\/gitlab\/Epic\//, ''), 10);
return axios
.post(issuesEndpoint, { epic_id: epicId, title })
.then(() =>
dispatch('fetchItems', {
parentItem,
fetchPolicy: 'network-only',
}),
)
.catch(e => {
flash(__('Could not create issue'));
throw e;
});
};
export const fetchProjects = ({ state, commit }) =>
axios
.get(state.projectsEndpoint, {
params: {
include_subgroups: true,
order_by: 'last_activity_at',
with_issues_enabled: true,
with_shared: false,
},
})
.then(({ data }) => {
commit(types.SET_PROJECTS, data);
})
.catch(e => {
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 () => {};
...@@ -38,3 +38,5 @@ export const RECEIVE_CREATE_ITEM_SUCCESS = 'RECEIVE_CREATE_ITEM_SUCCESS'; ...@@ -38,3 +38,5 @@ export const RECEIVE_CREATE_ITEM_SUCCESS = 'RECEIVE_CREATE_ITEM_SUCCESS';
export const RECEIVE_CREATE_ITEM_FAILURE = 'RECEIVE_CREATE_ITEM_FAILURE'; 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';
...@@ -5,12 +5,20 @@ import * as types from './mutation_types'; ...@@ -5,12 +5,20 @@ import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_CONFIG]( [types.SET_INITIAL_CONFIG](
state, state,
{ epicsEndpoint, issuesEndpoint, autoCompleteEpics, autoCompleteIssues, userSignedIn }, {
epicsEndpoint,
issuesEndpoint,
autoCompleteEpics,
autoCompleteIssues,
projectsEndpoint,
userSignedIn,
},
) { ) {
state.epicsEndpoint = epicsEndpoint; state.epicsEndpoint = epicsEndpoint;
state.issuesEndpoint = issuesEndpoint; state.issuesEndpoint = issuesEndpoint;
state.autoCompleteEpics = autoCompleteEpics; state.autoCompleteEpics = autoCompleteEpics;
state.autoCompleteIssues = autoCompleteIssues; state.autoCompleteIssues = autoCompleteIssues;
state.projectsEndpoint = projectsEndpoint;
state.userSignedIn = userSignedIn; state.userSignedIn = userSignedIn;
}, },
...@@ -191,4 +199,8 @@ export default { ...@@ -191,4 +199,8 @@ export default {
// Insert at new position // Insert at new position
state.children[parentItem.reference].splice(newIndex, 0, targetItem); state.children[parentItem.reference].splice(newIndex, 0, targetItem);
}, },
[types.SET_PROJECTS](state, projects) {
state.projects = projects;
},
}; };
...@@ -3,6 +3,7 @@ export default () => ({ ...@@ -3,6 +3,7 @@ export default () => ({
parentItem: {}, parentItem: {},
epicsEndpoint: '', epicsEndpoint: '',
issuesEndpoint: '', issuesEndpoint: '',
projectsEndpoint: null,
userSignedIn: false, userSignedIn: false,
children: {}, children: {},
...@@ -38,4 +39,6 @@ export default () => ({ ...@@ -38,4 +39,6 @@ export default () => ({
parentItem: {}, parentItem: {},
item: {}, item: {},
}, },
projects: [],
}); });
import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlDropdownItem, GlFormInput } from '@gitlab/ui';
const projects = getJSONFixture('static/projects.json');
const GlDropdownStub = {
name: 'GlDropdown',
template: '<div><slot></slot></div>',
};
describe('CreateIssueForm', () => {
let wrapper;
const createWrapper = () => {
wrapper = shallowMount(CreateIssueForm, {
sync: false,
stubs: {
GlDropdown: GlDropdownStub,
},
propsData: {
projects,
},
});
};
const findButton = text =>
wrapper.findAll(GlButton).wrappers.find(button => button.text() === text);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const getDropdownToggleText = () => wrapper.find(GlDropdownStub).attributes().text;
const clickDropdownItem = index =>
findDropdownItems()
.at(index)
.vm.$emit('click');
it('renders projects dropdown', () => {
createWrapper();
expect(findDropdownItems().length).toBeGreaterThan(0);
expect(findDropdownItems().length).toBe(projects.length);
const itemTexts = findDropdownItems().wrappers.map(item => item.text());
itemTexts.forEach((text, index) => {
const project = projects[index];
expect(text).toContain(project.name);
expect(text).toContain(project.namespace.name);
});
});
it('uses selected project as dropdown button text', () => {
createWrapper();
expect(getDropdownToggleText()).toBe('Select a project');
clickDropdownItem(1);
return wrapper.vm.$nextTick().then(() => {
expect(getDropdownToggleText()).toBe(projects[1].name_with_namespace);
});
});
describe('cancel button', () => {
const clickCancel = () => findButton('Cancel').vm.$emit('click');
it('emits cancel event', () => {
createWrapper();
clickCancel();
expect(wrapper.emitted()).toEqual({ cancel: [[]] });
});
});
describe('submit button', () => {
const dummyTitle = 'some issue title';
const clickSubmit = () => findButton('Create issue').vm.$emit('click');
const fillTitle = title => wrapper.find(GlFormInput).vm.$emit('input', title);
it('does not emit submit if project is missing', () => {
createWrapper();
fillTitle(dummyTitle);
clickSubmit();
expect(wrapper.emitted()).toEqual({});
});
it('does not emit submit if title is missing', () => {
createWrapper();
clickDropdownItem(1);
clickSubmit();
expect(wrapper.emitted()).toEqual({});
});
it('emits submit event for filled form', () => {
createWrapper();
fillTitle(dummyTitle);
clickDropdownItem(1);
clickSubmit();
const issuesEndpoint = projects[1]._links.issues;
const expectedParams = [{ issuesEndpoint, title: dummyTitle }];
expect(wrapper.emitted()).toEqual({ submit: [expectedParams] });
});
});
});
...@@ -8,7 +8,12 @@ import { issuableTypesMap } from 'ee/related_issues/constants'; ...@@ -8,7 +8,12 @@ import { issuableTypesMap } from 'ee/related_issues/constants';
import AddItemForm from 'ee/related_issues/components/add_issuable_form.vue'; import AddItemForm from 'ee/related_issues/components/add_issuable_form.vue';
import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue'; import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue';
import IssueActionsSplitButton from 'ee/related_items_tree/components/issue_actions_split_button.vue'; import IssueActionsSplitButton from 'ee/related_items_tree/components/issue_actions_split_button.vue';
import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { getJSONFixture } from 'helpers/fixtures';
// https://gitlab.com/gitlab-org/gitlab/issues/118456
import { import {
mockInitialConfig, mockInitialConfig,
mockParentItem, mockParentItem,
...@@ -16,6 +21,8 @@ import { ...@@ -16,6 +21,8 @@ import {
const localVue = createLocalVue(); const localVue = createLocalVue();
const mockProjects = getJSONFixture('static/projects.json');
const createComponent = () => { const createComponent = () => {
const store = createDefaultStore(); const store = createDefaultStore();
...@@ -30,14 +37,25 @@ const createComponent = () => { ...@@ -30,14 +37,25 @@ const createComponent = () => {
}; };
describe('RelatedItemsTreeApp', () => { describe('RelatedItemsTreeApp', () => {
let axiosMock;
let wrapper; let wrapper;
const findAddItemForm = () => wrapper.find(AddItemForm); const findAddItemForm = () => wrapper.find(AddItemForm);
const findCreateIssueForm = () => wrapper.find(CreateIssueForm); const findCreateIssueForm = () => wrapper.find(CreateIssueForm);
const findIssueActionsSplitButton = () => wrapper.find(IssueActionsSplitButton); const findIssueActionsSplitButton = () => wrapper.find(IssueActionsSplitButton);
const showCreateIssueForm = () => {
findIssueActionsSplitButton().vm.$emit('showCreateIssueForm');
return axios.waitFor(mockInitialConfig.projectsEndpoint).then(() => wrapper.vm.$nextTick());
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(mockInitialConfig.projectsEndpoint).replyOnce(200, mockProjects);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
axiosMock.restore();
}); });
describe('methods', () => { describe('methods', () => {
...@@ -266,20 +284,17 @@ describe('RelatedItemsTreeApp', () => { ...@@ -266,20 +284,17 @@ describe('RelatedItemsTreeApp', () => {
it('shows create item form', () => { it('shows create item form', () => {
expect(findCreateIssueForm().exists()).toBe(false); expect(findCreateIssueForm().exists()).toBe(false);
findIssueActionsSplitButton().vm.$emit('showCreateIssueForm'); return showCreateIssueForm().then(() => {
const form = findCreateIssueForm();
return wrapper.vm.$nextTick().then(() => { expect(form.exists()).toBe(true);
expect(findCreateIssueForm().exists()).toBe(true); expect(form.props().projects).toBe(mockProjects);
}); });
}); });
}); });
describe('after create issue form emitted cancel event', () => { describe('after create issue form emitted cancel event', () => {
beforeEach(() => { beforeEach(() => showCreateIssueForm());
findIssueActionsSplitButton().vm.$emit('showCreateIssueForm');
return wrapper.vm.$nextTick();
});
it('hides the form', () => { it('hides the form', () => {
expect(findCreateIssueForm().exists()).toBe(true); expect(findCreateIssueForm().exists()).toBe(true);
...@@ -291,5 +306,24 @@ describe('RelatedItemsTreeApp', () => { ...@@ -291,5 +306,24 @@ describe('RelatedItemsTreeApp', () => {
}); });
}); });
}); });
describe('after create issue form emitted submit event', () => {
beforeEach(() => showCreateIssueForm());
it('dispatches createNewIssue action', () => {
const issuesEndpoint = `${TEST_HOST}/issues`;
axiosMock.onPost(issuesEndpoint).replyOnce(200, {});
const params = {
issuesEndpoint,
title: 'some new issue',
};
findCreateIssueForm().vm.$emit('submit', params);
return axios.waitFor(issuesEndpoint).then(({ data }) => {
expect(JSON.parse(data).title).toBe(params.title);
});
});
});
}); });
}); });
import { TEST_HOST } from 'spec/test_constants';
export const mockInitialConfig = { export const mockInitialConfig = {
epicsEndpoint: 'http://test.host', epicsEndpoint: `${TEST_HOST}/epics`,
issuesEndpoint: 'http://test.host', issuesEndpoint: `${TEST_HOST}/issues`,
projectsEndpoint: `${TEST_HOST}/projects`,
autoCompleteEpics: true, autoCompleteEpics: true,
autoCompleteIssues: false, autoCompleteIssues: false,
userSignedIn: true, userSignedIn: true,
}; };
export const mockParentItem = { export const mockParentItem = {
id: 'gid://gitlab/Epic/42',
iid: 1, iid: 1,
fullPath: 'gitlab-org', fullPath: 'gitlab-org',
title: 'Some sample epic', title: 'Some sample epic',
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import createDefaultState from 'ee/related_items_tree/store/state'; import createDefaultState from 'ee/related_items_tree/store/state';
import * as actions from 'ee/related_items_tree/store/actions'; import actionsModule, * as actions from 'ee/related_items_tree/store/actions';
import * as types from 'ee/related_items_tree/store/mutation_types'; import * as types from 'ee/related_items_tree/store/mutation_types';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'spec/test_constants';
import { import {
mockInitialConfig, mockInitialConfig,
...@@ -1315,6 +1316,95 @@ describe('RelatedItemTree', () => { ...@@ -1315,6 +1316,95 @@ describe('RelatedItemTree', () => {
); );
}); });
}); });
describe('createNewIssue', () => {
const issuesEndpoint = `${TEST_HOST}/issues`;
const title = 'new issue title';
const epicId = 42;
const parentItem = {
id: `gid://gitlab/Epic/${epicId}`,
};
const expectedRequest = jasmine.objectContaining({
data: JSON.stringify({
epic_id: epicId,
title,
}),
});
let flashSpy;
let axiosMock;
let requestSpy;
let context;
let payload;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
});
beforeEach(() => {
flashSpy = spyOnDependency(actionsModule, 'flash');
requestSpy = jasmine.createSpy('request');
axiosMock.onPost(issuesEndpoint).replyOnce(config => requestSpy(config));
context = {
state: {
parentItem,
},
dispatch: jasmine.createSpy('dispatch'),
};
payload = {
issuesEndpoint,
title,
};
});
describe('for successful request', () => {
beforeEach(() => {
requestSpy.and.returnValue([201, '']);
});
it('dispatches fetchItems', done => {
actions
.createNewIssue(context, payload)
.then(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
expect(context.dispatch).toHaveBeenCalledWith(
'fetchItems',
jasmine.objectContaining({ parentItem }),
);
expect(flashSpy).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
describe('for failed request', () => {
beforeEach(() => {
requestSpy.and.returnValue([500, '']);
});
it('fails and shows flash message', done => {
actions
.createNewIssue(context, payload)
.then(() => done.fail('expected action to throw error!'))
.catch(() => {
expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
expect(context.dispatch).not.toHaveBeenCalled();
expect(flashSpy).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
});
}); });
}); });
}); });
...@@ -4994,12 +4994,18 @@ msgstr "" ...@@ -4994,12 +4994,18 @@ 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 ""
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 remove the trigger." msgid "Could not remove the trigger."
msgstr "" msgstr ""
...@@ -10007,6 +10013,9 @@ msgstr "" ...@@ -10007,6 +10013,9 @@ msgstr ""
msgid "IssuesAnalytics|Total:" msgid "IssuesAnalytics|Total:"
msgstr "" msgstr ""
msgid "Issue|Title"
msgstr ""
msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected." msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected."
msgstr "" msgstr ""
...@@ -11733,6 +11742,9 @@ msgstr "" ...@@ -11733,6 +11742,9 @@ msgstr ""
msgid "New issue" msgid "New issue"
msgstr "" msgstr ""
msgid "New issue title"
msgstr ""
msgid "New label" msgid "New label"
msgstr "" msgstr ""
......
...@@ -99,6 +99,15 @@ ...@@ -99,6 +99,15 @@
"access_level": 50, "access_level": 50,
"notification_level": 3 "notification_level": 3
} }
},
"_links": {
"self": "https://gitlab.com/api/v4/projects/278964",
"issues": "https://gitlab.com/api/v4/projects/278964/issues",
"merge_requests": "https://gitlab.com/api/v4/projects/278964/merge_requests",
"repo_branches": "https://gitlab.com/api/v4/projects/278964/repository/branches",
"labels": "https://gitlab.com/api/v4/projects/278964/labels",
"events": "https://gitlab.com/api/v4/projects/278964/events",
"members": "https://gitlab.com/api/v4/projects/278964/members"
} }
}, { }, {
"id": 7, "id": 7,
......
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