Commit b01ff4b4 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'kp-issuable-list-enhancements' into 'master'

Enhancements for `issuable_list` app

See merge request gitlab-org/gitlab!45897
parents b0961026 823c9460
<script> <script>
import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui'; import { GlLink, GlIcon, GlLabel, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
export default { export default {
components: { components: {
GlLink, GlLink,
GlIcon,
GlLabel, GlLabel,
IssuableAssignees,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -24,25 +29,33 @@ export default { ...@@ -24,25 +29,33 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
enableLabelPermalinks: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
author() { author() {
return this.issuable.author; return this.issuable.author;
}, },
authorId() { authorId() {
const id = parseInt(this.author.id, 10); return getIdFromGraphQLId(`${this.author.id}`);
},
if (Number.isNaN(id)) { isIssuableUrlExternal() {
return this.author.id.includes('gid') // Check if URL is relative, which means it is internal.
? this.author.id.split('gid://gitlab/User/').pop() if (!/^https?:\/\//g.test(this.issuable.webUrl)) {
: ''; return false;
} }
// In case URL is absolute, it may or may not be internal,
return id; // hence use `gon.gitlab_url` which is current instance domain.
return !this.issuable.webUrl.includes(gon.gitlab_url);
}, },
labels() { labels() {
return this.issuable.labels?.nodes || this.issuable.labels || []; return this.issuable.labels?.nodes || this.issuable.labels || [];
}, },
assignees() {
return this.issuable.assignees || [];
},
createdAt() { createdAt() {
return sprintf(__('created %{timeAgo}'), { return sprintf(__('created %{timeAgo}'), {
timeAgo: getTimeago().format(this.issuable.createdAt), timeAgo: getTimeago().format(this.issuable.createdAt),
...@@ -53,11 +66,33 @@ export default { ...@@ -53,11 +66,33 @@ export default {
timeAgo: getTimeago().format(this.issuable.updatedAt), timeAgo: getTimeago().format(this.issuable.updatedAt),
}); });
}, },
issuableTitleProps() {
if (this.isIssuableUrlExternal) {
return {
target: '_blank',
};
}
return {};
},
}, },
methods: { methods: {
hasSlotContents(slotName) {
return Boolean(this.$slots[slotName]);
},
scopedLabel(label) { scopedLabel(label) {
return isScopedLabel(label); return isScopedLabel(label);
}, },
labelTitle(label) {
return label.title || label.name;
},
labelTarget(label) {
if (this.enableLabelPermalinks) {
const key = encodeURIComponent('label_name[]');
const value = encodeURIComponent(this.labelTitle(label));
return `?${key}=${value}`;
}
return '#';
},
/** /**
* This is needed as an independent method since * This is needed as an independent method since
* when user changes current page, `$refs.authorLink` * when user changes current page, `$refs.authorLink`
...@@ -74,17 +109,20 @@ export default { ...@@ -74,17 +109,20 @@ export default {
</script> </script>
<template> <template>
<li class="issue"> <li class="issue px-3">
<div class="issue-box"> <div class="issue-box">
<div class="issuable-info-container"> <div class="issuable-info-container">
<div class="issuable-main-info"> <div class="issuable-main-info">
<div data-testid="issuable-title" class="issue-title title"> <div data-testid="issuable-title" class="issue-title title">
<span class="issue-title-text" dir="auto"> <span class="issue-title-text" dir="auto">
<gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link> <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
>{{ issuable.title }}<gl-icon v-if="isIssuableUrlExternal" name="external-link"
/></gl-link>
</span> </span>
</div> </div>
<div class="issuable-info"> <div class="issuable-info">
<span data-testid="issuable-reference" class="issuable-reference" <slot v-if="hasSlotContents('reference')" name="reference"></slot>
<span v-else data-testid="issuable-reference" class="issuable-reference"
>{{ issuableSymbol }}{{ issuable.iid }}</span >{{ issuableSymbol }}{{ issuable.iid }}</span
> >
<span class="issuable-authored d-none d-sm-inline-block"> <span class="issuable-authored d-none d-sm-inline-block">
...@@ -113,15 +151,30 @@ export default { ...@@ -113,15 +151,30 @@ export default {
v-for="(label, index) in labels" v-for="(label, index) in labels"
:key="index" :key="index"
:background-color="label.color" :background-color="label.color"
:title="label.title" :title="labelTitle(label)"
:description="label.description" :description="label.description"
:scoped="scopedLabel(label)" :scoped="scopedLabel(label)"
:target="labelTarget(label)"
:class="{ 'gl-ml-2': index }" :class="{ 'gl-ml-2': index }"
size="sm" size="sm"
/> />
</div> </div>
</div> </div>
<div class="issuable-meta"> <div class="issuable-meta">
<ul v-if="hasSlotContents('status') || issuable.assignees" class="controls">
<li v-if="hasSlotContents('status')" class="issuable-status">
<slot name="status"></slot>
</li>
<li v-if="assignees.length" class="gl-display-flex">
<issuable-assignees
:assignees="issuable.assignees"
:icon-size="16"
:max-visible="4"
img-css-classes="gl-mr-2!"
class="gl-align-items-center gl-display-flex gl-ml-3"
/>
</li>
</ul>
<div <div
data-testid="issuable-updated-at" data-testid="issuable-updated-at"
class="float-right issuable-updated-at d-none d-sm-inline-block" class="float-right issuable-updated-at d-none d-sm-inline-block"
......
<script> <script>
import { GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import IssuableTabs from './issuable_tabs.vue'; import IssuableTabs from './issuable_tabs.vue';
...@@ -35,6 +36,11 @@ export default { ...@@ -35,6 +36,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
urlParams: {
type: Object,
required: false,
default: () => ({}),
},
initialFilterValue: { initialFilterValue: {
type: Array, type: Array,
required: false, required: false,
...@@ -55,7 +61,8 @@ export default { ...@@ -55,7 +61,8 @@ export default {
}, },
tabCounts: { tabCounts: {
type: Object, type: Object,
required: true, required: false,
default: null,
}, },
currentTab: { currentTab: {
type: String, type: String,
...@@ -81,6 +88,11 @@ export default { ...@@ -81,6 +88,11 @@ export default {
required: false, required: false,
default: 20, default: 20,
}, },
totalPages: {
type: Number,
required: false,
default: 0,
},
currentPage: { currentPage: {
type: Number, type: Number,
required: false, required: false,
...@@ -96,6 +108,26 @@ export default { ...@@ -96,6 +108,26 @@ export default {
required: false, required: false,
default: 2, default: 2,
}, },
enableLabelPermalinks: {
type: Boolean,
required: false,
default: true,
},
},
watch: {
urlParams: {
deep: true,
immediate: true,
handler(params) {
if (Object.keys(params).length) {
updateHistory({
url: setUrlParams(params, window.location.href, true),
title: document.title,
replace: true,
});
}
},
},
}, },
}; };
</script> </script>
...@@ -135,12 +167,21 @@ export default { ...@@ -135,12 +167,21 @@ export default {
:key="issuable.id" :key="issuable.id"
:issuable-symbol="issuableSymbol" :issuable-symbol="issuableSymbol"
:issuable="issuable" :issuable="issuable"
/> :enable-label-permalinks="enableLabelPermalinks"
>
<template #reference>
<slot name="reference" :issuable="issuable"></slot>
</template>
<template #status>
<slot name="status" :issuable="issuable"></slot>
</template>
</issuable-item>
</ul> </ul>
<slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot> <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot>
<gl-pagination <gl-pagination
v-if="showPaginationControls" v-if="showPaginationControls"
:per-page="defaultPageSize" :per-page="defaultPageSize"
:total-items="totalPages"
:value="currentPage" :value="currentPage"
:prev-page="previousPage" :prev-page="previousPage"
:next-page="nextPage" :next-page="nextPage"
......
...@@ -14,7 +14,8 @@ export default { ...@@ -14,7 +14,8 @@ export default {
}, },
tabCounts: { tabCounts: {
type: Object, type: Object,
required: true, required: false,
default: null,
}, },
currentTab: { currentTab: {
type: String, type: String,
...@@ -40,7 +41,7 @@ export default { ...@@ -40,7 +41,7 @@ export default {
> >
<template #title> <template #title>
<span :title="tab.titleTooltip">{{ tab.title }}</span> <span :title="tab.titleTooltip">{{ tab.title }}</span>
<gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{ <gl-badge v-if="tabCounts" variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
tabCounts[tab.name] tabCounts[tab.name]
}}</gl-badge> }}</gl-badge>
</template> </template>
......
import { __ } from '~/locale';
export const IssuableStates = {
Opened: 'opened',
Closed: 'closed',
All: 'all',
};
export const IssuableListTabs = [
{
id: 'state-opened',
name: IssuableStates.Opened,
title: __('Open'),
titleTooltip: __('Filter by issues that are currently opened.'),
},
{
id: 'state-closed',
name: IssuableStates.Closed,
title: __('Closed'),
titleTooltip: __('Filter by issues that are currently closed.'),
},
{
id: 'state-all',
name: IssuableStates.All,
title: __('All'),
titleTooltip: __('Show all issues.'),
},
];
export const AvailableSortOptions = [
{
id: 1,
title: __('Created date'),
sortDirection: {
descending: 'created_desc',
ascending: 'created_asc',
},
},
{
id: 2,
title: __('Last updated'),
sortDirection: {
descending: 'updated_desc',
ascending: 'updated_asc',
},
},
];
export const DEFAULT_PAGE_SIZE = 20;
...@@ -11517,6 +11517,9 @@ msgstr "" ...@@ -11517,6 +11517,9 @@ msgstr ""
msgid "Filter by issues that are currently closed." msgid "Filter by issues that are currently closed."
msgstr "" msgstr ""
msgid "Filter by issues that are currently opened."
msgstr ""
msgid "Filter by label" msgid "Filter by label"
msgstr "" msgstr ""
...@@ -24258,6 +24261,9 @@ msgstr "" ...@@ -24258,6 +24261,9 @@ msgstr ""
msgid "Show all activity" msgid "Show all activity"
msgstr "" msgstr ""
msgid "Show all issues."
msgstr ""
msgid "Show all members" msgid "Show all members"
msgstr "" msgstr ""
......
...@@ -2,28 +2,34 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,28 +2,34 @@ import { shallowMount } from '@vue/test-utils';
import { GlLink, GlLabel } from '@gitlab/ui'; import { GlLink, GlLabel } from '@gitlab/ui';
import IssuableItem from '~/issuable_list/components/issuable_item.vue'; import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data'; import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data';
const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable } = {}) => const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots = {} } = {}) =>
shallowMount(IssuableItem, { shallowMount(IssuableItem, {
propsData: { propsData: {
issuableSymbol, issuableSymbol,
issuable, issuable,
enableLabelPermalinks: true,
}, },
slots,
}); });
describe('IssuableItem', () => { describe('IssuableItem', () => {
const mockLabels = mockIssuable.labels.nodes; const mockLabels = mockIssuable.labels.nodes;
const mockAuthor = mockIssuable.author; const mockAuthor = mockIssuable.author;
const originalUrl = gon.gitlab_url;
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
gon.gitlab_url = 'http://0.0.0.0:3000';
wrapper = createComponent(); wrapper = createComponent();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
gon.gitlab_url = originalUrl;
}); });
describe('computed', () => { describe('computed', () => {
...@@ -38,8 +44,8 @@ describe('IssuableItem', () => { ...@@ -38,8 +44,8 @@ describe('IssuableItem', () => {
authorId | returnValue authorId | returnValue
${1} | ${1} ${1} | ${1}
${'1'} | ${1} ${'1'} | ${1}
${'gid://gitlab/User/1'} | ${'1'} ${'gid://gitlab/User/1'} | ${1}
${'foo'} | ${''} ${'foo'} | ${null}
`( `(
'returns $returnValue when value of `issuable.author.id` is $authorId', 'returns $returnValue when value of `issuable.author.id` is $authorId',
async ({ authorId, returnValue }) => { async ({ authorId, returnValue }) => {
...@@ -60,6 +66,30 @@ describe('IssuableItem', () => { ...@@ -60,6 +66,30 @@ describe('IssuableItem', () => {
); );
}); });
describe('isIssuableUrlExternal', () => {
it.each`
issuableWebUrl | urlType | returnValue
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
${'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/1'} | ${'absolute and internal'} | ${false}
${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
`(
'returns $returnValue when `issuable.webUrl` is $urlType',
async ({ issuableWebUrl, returnValue }) => {
wrapper.setProps({
issuable: {
...mockIssuable,
webUrl: issuableWebUrl,
},
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.isIssuableUrlExternal).toBe(returnValue);
},
);
});
describe('labels', () => { describe('labels', () => {
it('returns `issuable.labels.nodes` reference when it is available', () => { it('returns `issuable.labels.nodes` reference when it is available', () => {
expect(wrapper.vm.labels).toEqual(mockLabels); expect(wrapper.vm.labels).toEqual(mockLabels);
...@@ -92,6 +122,12 @@ describe('IssuableItem', () => { ...@@ -92,6 +122,12 @@ describe('IssuableItem', () => {
}); });
}); });
describe('assignees', () => {
it('returns `issuable.assignees` reference when it is available', () => {
expect(wrapper.vm.assignees).toBe(mockIssuable.assignees);
});
});
describe('createdAt', () => { describe('createdAt', () => {
it('returns string containing timeago string based on `issuable.createdAt`', () => { it('returns string containing timeago string based on `issuable.createdAt`', () => {
expect(wrapper.vm.createdAt).toContain('created'); expect(wrapper.vm.createdAt).toContain('created');
...@@ -120,6 +156,34 @@ describe('IssuableItem', () => { ...@@ -120,6 +156,34 @@ describe('IssuableItem', () => {
}, },
); );
}); });
describe('labelTitle', () => {
it.each`
label | propWithTitle | returnValue
${{ title: 'foo' }} | ${'title'} | ${'foo'}
${{ name: 'foo' }} | ${'name'} | ${'foo'}
`('returns string value of `label.$propWithTitle`', ({ label, returnValue }) => {
expect(wrapper.vm.labelTitle(label)).toBe(returnValue);
});
});
describe('labelTarget', () => {
it('returns target string for a provided label param when `enableLabelPermalinks` is true', () => {
expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe(
'?label_name%5B%5D=Documentation%20Update',
);
});
it('returns string "#" for a provided label param when `enableLabelPermalinks` is false', async () => {
wrapper.setProps({
enableLabelPermalinks: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe('#');
});
});
}); });
describe('template', () => { describe('template', () => {
...@@ -128,9 +192,28 @@ describe('IssuableItem', () => { ...@@ -128,9 +192,28 @@ describe('IssuableItem', () => {
expect(titleEl.exists()).toBe(true); expect(titleEl.exists()).toBe(true);
expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl); expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
expect(titleEl.find(GlLink).attributes('target')).not.toBeDefined();
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title); expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
}); });
it('renders issuable title with `target` set as "_blank" when issuable.webUrl is external', async () => {
wrapper.setProps({
issuable: {
...mockIssuable,
webUrl: 'http://jira.atlassian.net/browse/IG-1',
},
});
await wrapper.vm.$nextTick();
expect(
wrapper
.find('[data-testid="issuable-title"]')
.find(GlLink)
.attributes('target'),
).toBe('_blank');
});
it('renders issuable reference', () => { it('renders issuable reference', () => {
const referenceEl = wrapper.find('[data-testid="issuable-reference"]'); const referenceEl = wrapper.find('[data-testid="issuable-reference"]');
...@@ -138,6 +221,24 @@ describe('IssuableItem', () => { ...@@ -138,6 +221,24 @@ describe('IssuableItem', () => {
expect(referenceEl.text()).toBe(`#${mockIssuable.iid}`); expect(referenceEl.text()).toBe(`#${mockIssuable.iid}`);
}); });
it('renders issuable reference via slot', () => {
const wrapperWithRefSlot = createComponent({
issuableSymbol: '#',
issuable: mockIssuable,
slots: {
reference: `
<b class="js-reference">${mockIssuable.iid}</b>
`,
},
});
const referenceEl = wrapperWithRefSlot.find('.js-reference');
expect(referenceEl.exists()).toBe(true);
expect(referenceEl.text()).toBe(`${mockIssuable.iid}`);
wrapperWithRefSlot.destroy();
});
it('renders issuable createdAt info', () => { it('renders issuable createdAt info', () => {
const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]'); const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]');
...@@ -151,7 +252,7 @@ describe('IssuableItem', () => { ...@@ -151,7 +252,7 @@ describe('IssuableItem', () => {
expect(authorEl.exists()).toBe(true); expect(authorEl.exists()).toBe(true);
expect(authorEl.attributes()).toMatchObject({ expect(authorEl.attributes()).toMatchObject({
'data-user-id': wrapper.vm.authorId, 'data-user-id': `${wrapper.vm.authorId}`,
'data-username': mockAuthor.username, 'data-username': mockAuthor.username,
'data-name': mockAuthor.name, 'data-name': mockAuthor.name,
'data-avatar-url': mockAuthor.avatarUrl, 'data-avatar-url': mockAuthor.avatarUrl,
...@@ -170,10 +271,40 @@ describe('IssuableItem', () => { ...@@ -170,10 +271,40 @@ describe('IssuableItem', () => {
title: mockLabels[0].title, title: mockLabels[0].title,
description: mockLabels[0].description, description: mockLabels[0].description,
scoped: false, scoped: false,
target: wrapper.vm.labelTarget(mockLabels[0]),
size: 'sm', size: 'sm',
}); });
}); });
it('renders issuable status via slot', () => {
const wrapperWithStatusSlot = createComponent({
issuableSymbol: '#',
issuable: mockIssuable,
slots: {
status: `
<b class="js-status">${mockIssuable.state}</b>
`,
},
});
const statusEl = wrapperWithStatusSlot.find('.js-status');
expect(statusEl.exists()).toBe(true);
expect(statusEl.text()).toBe(`${mockIssuable.state}`);
wrapperWithStatusSlot.destroy();
});
it('renders issuable-assignees component', () => {
const assigneesEl = wrapper.find(IssuableAssignees);
expect(assigneesEl.exists()).toBe(true);
expect(assigneesEl.props()).toMatchObject({
assignees: mockIssuable.assignees,
iconSize: 16,
maxVisible: 4,
});
});
it('renders issuable updatedAt info', () => { it('renders issuable updatedAt info', () => {
const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]'); const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import IssuableListRoot from '~/issuable_list/components/issuable_list_root.vue'; import IssuableListRoot from '~/issuable_list/components/issuable_list_root.vue';
import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue'; import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
import IssuableItem from '~/issuable_list/components/issuable_item.vue'; import IssuableItem from '~/issuable_list/components/issuable_item.vue';
...@@ -32,6 +34,29 @@ describe('IssuableListRoot', () => { ...@@ -32,6 +34,29 @@ describe('IssuableListRoot', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('watch', () => {
describe('urlParams', () => {
it('updates window URL reflecting props within `urlParams`', async () => {
const urlParams = {
state: 'closed',
sort: 'updated_asc',
page: 1,
search: 'foo',
};
wrapper.setProps({
urlParams,
});
await wrapper.vm.$nextTick();
expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${urlParams.state}&sort=${urlParams.sort}&page=${urlParams.page}&search=${urlParams.search}`,
);
});
});
});
describe('template', () => { describe('template', () => {
it('renders component container element with class "issuable-list-container"', () => { it('renders component container element with class "issuable-list-container"', () => {
expect(wrapper.classes()).toContain('issuable-list-container'); expect(wrapper.classes()).toContain('issuable-list-container');
...@@ -114,6 +139,7 @@ describe('IssuableListRoot', () => { ...@@ -114,6 +139,7 @@ describe('IssuableListRoot', () => {
it('renders gl-pagination when `showPaginationControls` prop is true', async () => { it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
wrapper.setProps({ wrapper.setProps({
showPaginationControls: true, showPaginationControls: true,
totalPages: 10,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -125,6 +151,7 @@ describe('IssuableListRoot', () => { ...@@ -125,6 +151,7 @@ describe('IssuableListRoot', () => {
value: 1, value: 1,
prevPage: 0, prevPage: 0,
nextPage: 2, nextPage: 2,
totalItems: 10,
align: 'center', align: 'center',
}); });
}); });
......
...@@ -51,6 +51,7 @@ export const mockIssuable = { ...@@ -51,6 +51,7 @@ export const mockIssuable = {
labels: { labels: {
nodes: mockLabels, nodes: mockLabels,
}, },
assignees: [mockAuthor],
}; };
export const mockIssuables = [ export const mockIssuables = [
......
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