summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorConstance Okoghenun <constanceokoghenun@gmail.com>2019-06-10 22:53:21 +0200
committerMartin Hanzel <mhanzel@gitlab.com>2019-08-08 11:43:35 -0400
commite8f23e84af01b0c0e797aeaa364ac3e985648b04 (patch)
treeb38c233db47cd09d112dce3ddfa41304657bcb84
parent79bff3ee7a0d2d91faedeadb1965966f7551b62c (diff)
downloadgitlab-ce-mh/vue-issuables-list.tar.gz
Squash commits from 57402-issues-list-vue-componentmh/vue-issuables-list
Added locale changes to issues list component Added constants and missing i18n Moved items to computed properties Added MAX_ASSIGNEES_RENDER constant Apply suggestion to app/assets/javascripts/issues/components/issue.vue Apply suggestion to app/assets/javascripts/issues/index.js Apply suggestion to app/assets/javascripts/issues/services/issues_service.js Apply suggestion to app/assets/javascripts/issues/stores/modules/issues_list/actions.js Updated image_path to take from haml Fixed tests to account for empty state props Updated gitlab.pot
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js23
-rw-r--r--app/assets/javascripts/issues/components/empty_state.vue78
-rw-r--r--app/assets/javascripts/issues/components/issue.vue279
-rw-r--r--app/assets/javascripts/issues/components/issues_app.vue184
-rw-r--r--app/assets/javascripts/issues/components/loading_state.vue17
-rw-r--r--app/assets/javascripts/issues/constants.js9
-rw-r--r--app/assets/javascripts/issues/index.js53
-rw-r--r--app/assets/javascripts/issues/issues_filtered_search.js32
-rw-r--r--app/assets/javascripts/issues/services/issues_service.js13
-rw-r--r--app/assets/javascripts/issues/stores/index.js14
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/actions.js105
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/getters.js29
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/index.js12
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/mutation_types.js6
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/mutations.js24
-rw-r--r--app/assets/javascripts/issues/stores/modules/issues_list/state.js12
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/frontend/helpers/url_util_helper.js7
-rw-r--r--spec/frontend/issues/components/issue_spec.js222
-rw-r--r--spec/frontend/issues/components/issues_app_spec.js234
-rw-r--r--spec/frontend/issues/mock_data.js1491
-rw-r--r--spec/frontend/issues/stores/modules/issues_list/actions_spec.js111
-rw-r--r--spec/frontend/issues/stores/modules/issues_list/getters_spec.js27
-rw-r--r--spec/frontend/issues/stores/modules/issues_list/mutations_spec.js66
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js8
25 files changed, 3055 insertions, 16 deletions
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 74150ce3a8b..7a74611acde 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,11 +1,10 @@
-/* eslint-disable class-methods-use-this, no-new */
-
import $ from 'jquery';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
+import issuesListStore from '~/issues/stores';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -14,6 +13,7 @@ const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-si
export default class IssuableBulkUpdateSidebar {
constructor() {
+ this.issuableBulkUpdateActions = IssuableBulkUpdateActions;
this.initDomElements();
this.bindEvents();
this.initDropdowns();
@@ -44,14 +44,14 @@ export default class IssuableBulkUpdateSidebar {
}
initDropdowns() {
- new LabelsSelect();
- new MilestoneSelect();
- issueStatusSelect();
- subscriptionSelect();
+ this.labelSelect = new LabelsSelect();
+ this.labelSelect = new MilestoneSelect();
+ this.issueStatusSelect = issueStatusSelect();
+ this.subscriptionSelect = subscriptionSelect();
}
setupBulkUpdateActions() {
- IssuableBulkUpdateActions.setOriginalDropdownData();
+ this.issuableBulkUpdateActions.setOriginalDropdownData();
}
updateFormState() {
@@ -60,7 +60,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
- IssuableBulkUpdateActions.setOriginalDropdownData();
+ this.issuableBulkUpdateActions.setOriginalDropdownData();
}
prepForSubmit() {
@@ -107,7 +107,12 @@ export default class IssuableBulkUpdateSidebar {
toggleCheckboxDisplay(show) {
this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
- this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
+
+ if (gon.features && gon.features.issuesVueComponent) {
+ issuesListStore.dispatch('issuesList/setBulkUpdateState', show);
+ } else {
+ this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
+ }
}
toggleOtherFiltersDisabled(disable) {
diff --git a/app/assets/javascripts/issues/components/empty_state.vue b/app/assets/javascripts/issues/components/empty_state.vue
new file mode 100644
index 00000000000..e6aa9c6a77b
--- /dev/null
+++ b/app/assets/javascripts/issues/components/empty_state.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { ISSUE_STATES } from '../constants';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ hasFilters: {
+ type: Boolean,
+ required: true,
+ },
+ state: {
+ type: String,
+ required: true,
+ },
+ buttonPath: {
+ type: String,
+ required: true,
+ },
+ loadingDisabled: {
+ type: Boolean,
+ required: true,
+ },
+ svgImagePath: {
+ type: String,
+ required: true,
+ },
+ svgLoadingDisabledImagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ ISSUE_STATES,
+ };
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="loadingDisabled"
+ :title="__('Please select at least one filter to see results')"
+ :svg-path="svgLoadingDisabledImagePath"
+ />
+ <gl-empty-state
+ v-else-if="hasFilters"
+ :title="__('Sorry, your filter produced no results')"
+ :description="__('To widen your search, change or remove filters above')"
+ :svg-path="svgImagePath"
+ />
+ <gl-empty-state
+ v-else-if="state === ISSUE_STATES.OPENED"
+ :title="__('There are no open issues')"
+ :description="__('To keep this project going, create a new issue')"
+ :primary-button-link="buttonPath"
+ :primary-button-text="__('New issue')"
+ :svg-path="svgImagePath"
+ />
+ <gl-empty-state
+ v-else-if="state === ISSUE_STATES.CLOSED"
+ :title="__('There are no closed issues')"
+ :svg-path="svgImagePath"
+ />
+ <gl-empty-state
+ v-else
+ :title="__('There are no issues to show')"
+ :description="
+ __(
+ 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
+ )
+ "
+ :svg-path="svgImagePath"
+ />
+</template>
diff --git a/app/assets/javascripts/issues/components/issue.vue b/app/assets/javascripts/issues/components/issue.vue
new file mode 100644
index 00000000000..13f16fefa60
--- /dev/null
+++ b/app/assets/javascripts/issues/components/issue.vue
@@ -0,0 +1,279 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltipDirective } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { getDayDifference } from '~/lib/utils/datetime_utility';
+import { ISSUE_STATES, MAX_ASSIGNEES_RENDER } from '../constants';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TimeAgoTooltip,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ isBulkUpdating: {
+ type: Boolean,
+ required: true,
+ },
+ canBulkUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ issueClassesList() {
+ const classList = ['issue'];
+ const issueCreatedToday = !getDayDifference(new Date(this.issue.created_at), new Date());
+
+ if (this.issueIsClosed) {
+ classList.push('closed');
+ }
+
+ if (issueCreatedToday) {
+ classList.push('today');
+ }
+
+ return classList;
+ },
+ formatDueDate() {
+ return dateFormat(this.issue.due_date, 'mmm d, yyyy');
+ },
+ isIssueOverDue() {
+ return getDayDifference(new Date(), new Date(this.issue.due_date)) < 0;
+ },
+ shouldAssigneeRenderCounter() {
+ return this.issue.assignees.length - MAX_ASSIGNEES_RENDER > 0;
+ },
+ moreAssigneesCount() {
+ return this.issue.assignees.length - MAX_ASSIGNEES_RENDER;
+ },
+ assigneeCounterTooltip() {
+ return sprintf('+%{moreAssigneesCount} more assignees', {
+ moreAssigneesCount: this.moreAssigneesCount,
+ });
+ },
+ issueCommentsURL() {
+ return `${this.issue.web_url}#notes`;
+ },
+ bulkUpdateId() {
+ return `selected_issue_${this.issue.id}`;
+ },
+ issueIsClosed() {
+ return this.issue.state === ISSUE_STATES.CLOSED;
+ },
+ assigneesToRender() {
+ return this.issue.assignees.filter((assignee, index) => index < MAX_ASSIGNEES_RENDER);
+ },
+ hasRelatedMergeRequests() {
+ return this.issue.merge_requests_count > 0;
+ },
+ hasUpvotes() {
+ return this.issue.upvotes > 0;
+ },
+ hasDownvotes() {
+ return this.issue.downvotes > 0;
+ },
+ hasComments() {
+ return this.issue.note_count < 0;
+ },
+ },
+ methods: {
+ getAvatarTitle(assignee) {
+ return sprintf('Assigned to %{name}', { name: assignee.name });
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.text_color,
+ };
+ },
+ },
+ confidentialText: __('Confidential'),
+};
+</script>
+<template>
+ <li :id="`issue_${issue.id}`" :url="issue.web_url" :class="issueClassesList" data-labels="[]">
+ <div class="issue-box">
+ <div v-if="canBulkUpdate" class="issue-check" :class="{ hidden: !isBulkUpdating }">
+ <input
+ :id="bulkUpdateId"
+ type="checkbox"
+ class="selected-issuable"
+ :name="bulkUpdateId"
+ :data-id="issue.id"
+ />
+ </div>
+ <div class="issuable-info-container">
+ <div class="issuable-main-info">
+ <div class="issue-title title">
+ <span class="issue-title-text">
+ <span
+ v-if="issue.confidential"
+ v-gl-tooltip
+ :title="$options.confidentialText"
+ :aria-label="$options.confidentialText"
+ >
+ <icon name="eye-slash" class="align-text-bottom js-issue-confidential-icon" />
+ </span>
+ <a :href="issue.web_url">{{ issue.title }}</a>
+ <span v-if="issue.has_tasks" class="task-status d-none d-sm-inline-block">
+ &nbsp;
+ {{ issue.task_status }}
+ </span>
+ </span>
+ </div>
+ <div class="issuable-info">
+ <span class="issuable-reference">{{ issue.reference_path }}</span>
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot; {{ __('opened') }}
+ <time-ago-tooltip :time="issue.created_at" tooltip-placement="bottom" />
+ by
+ <a
+ class="author-link js-user-link"
+ :data-user-id="issue.author.id"
+ :data-username="issue.author.username"
+ :data-name="issue.author.name"
+ :href="issue.author.web_url"
+ >
+ <span class="author">{{ issue.author.name }}</span>
+ </a>
+ </span>
+ <span v-if="issue.milestone" class="issuable-milestone d-none d-sm-inline-block">
+ &nbsp;
+ <a :href="issue.milestone.web_url">
+ <icon name="clock" class="align-text-bottom" />
+ {{ issue.milestone.title }}
+ </a>
+ </span>
+ <span
+ v-if="issue.due_date"
+ v-gl-tooltip
+ class="issuable-due-date d-none d-sm-inline-block"
+ :title="__('Due date')"
+ :class="{ cred: isIssueOverDue }"
+ >
+ &nbsp;
+ <icon name="calendar" class="align-text-bottom" />
+ {{ formatDueDate }}
+ </span>
+ <span v-if="issue.labels">
+ <span
+ v-for="label in issue.labels"
+ :key="label.id"
+ ref="button"
+ class="label-link append-right-4 cursor-pointer"
+ @click="$emit('issueLabelClicked', label.name)"
+ >
+ <span
+ v-gl-tooltip
+ class="badge color-label"
+ :style="labelStyle(label)"
+ :title="label.description"
+ >
+ {{ label.name }}
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="issue.weight"
+ v-gl-tooltip
+ :title="__('Weight')"
+ class="issuable-weight d-none d-sm-inline-block"
+ >
+ <icon name="weight" class="align-text-bottom issue-weight-icon" />
+ {{ issue.weight }}
+ </span>
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <ul class="controls">
+ <li v-if="issueIsClosed" class="issuable-status">{{ __('CLOSED') }}</li>
+ <li v-if="issue.assignees.length">
+ <user-avatar-link
+ v-for="assignee in assigneesToRender"
+ :key="assignee.id"
+ :link-href="assignee.web_url"
+ :img-alt="getAvatarTitle(assignee)"
+ :img-src="assignee.avatar_url"
+ :img-size="16"
+ :tooltip-text="getAvatarTitle(assignee)"
+ tooltip-placement="bottom"
+ />
+ <span
+ v-if="shouldAssigneeRenderCounter"
+ v-gl-tooltip
+ :title="assigneeCounterTooltip"
+ class="avatar-counter"
+ data-placement="bottom"
+ >
+ +{{ moreAssigneesCount }}
+ </span>
+ </li>
+ <li
+ v-if="hasRelatedMergeRequests"
+ v-gl-tooltip
+ class="issuable-mr d-none d-sm-block"
+ :title="__('Related merge requests')"
+ >
+ <icon name="merge-request" class="align-text-bottom icon-merge-request-unmerged" />
+ {{ issue.merge_requests_count }}
+ </li>
+
+ <li
+ v-if="hasUpvotes"
+ v-gl-tooltip
+ class="issuable-upvotes d-none d-sm-block"
+ :title="__('Upvotes')"
+ >
+ <icon name="thumb-up" class="align-text-bottom" />
+ {{ issue.upvotes }}
+ </li>
+
+ <li
+ v-if="hasDownvotes"
+ v-gl-tooltip
+ class="issuable-downvotes d-none d-sm-block"
+ :title="__('Downvotes')"
+ >
+ <icon name="thumb-down" class="align-text-bottom" />
+ {{ issue.downvotes }}
+ </li>
+
+ <li class="issuable-comments d-none d-sm-block">
+ <a
+ v-gl-tooltip
+ :href="issueCommentsURL"
+ :class="{ 'no-comments': hasComments }"
+ :title="__('Comments')"
+ >
+ <icon name="comments" class="align-text-bottom" />
+ {{ issue.note_count || 0 }}
+ </a>
+ </li>
+ </ul>
+ <div class="float-right issuable-updated-at d-none d-sm-inline-block">
+ <span>
+ updated
+ <time-ago-tooltip
+ :time="issue.updated_at"
+ tooltip-placement="bottom"
+ css-class="issue_update_ago"
+ />
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issues/components/issues_app.vue b/app/assets/javascripts/issues/components/issues_app.vue
new file mode 100644
index 00000000000..3272b5da321
--- /dev/null
+++ b/app/assets/javascripts/issues/components/issues_app.vue
@@ -0,0 +1,184 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlPagination } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import {
+ objectToQueryString,
+ urlParamsToObject,
+ scrollToElement,
+ isInProjectPage,
+ isInGroupsPage,
+ getPagePath,
+} from '~/lib/utils/common_utils';
+import Issue from './issue.vue';
+import IssuesEmptyState from './empty_state.vue';
+import IssuesLoadingState from './loading_state.vue';
+import { ISSUE_STATES, ACTIVE_TAB_CLASS, ISSUES_PER_PAGE, DASHBOARD_PAGE_NAME } from '../constants';
+
+export default {
+ components: {
+ IssuesLoadingState,
+ IssuesEmptyState,
+ GlPagination,
+ Issue,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ canBulkUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ createPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ issuableIndex: {
+ type: Object,
+ required: true,
+ },
+ filteredSearch: {
+ type: Object,
+ required: true,
+ },
+ projectSelect: {
+ type: Function,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyStateLoadingDisabledSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ ISSUES_PER_PAGE,
+ isInGroupsPage: isInGroupsPage(),
+ isInProjectPage: isInProjectPage(),
+ isInDashboardPage: getPagePath() === DASHBOARD_PAGE_NAME,
+ isLoadingDisabled: false,
+ };
+ },
+ computed: {
+ ...mapState('issuesList', ['issues', 'loading', 'isBulkUpdating', 'currentPage', 'totalItems']),
+ ...mapGetters('issuesList', ['hasFilters', 'appliedFilters']),
+
+ hasIssues() {
+ return !this.isLoadingDisabled && !this.loading && this.issues && this.issues.length > 0;
+ },
+ },
+ watch: {
+ appliedFilters() {
+ this.loadIssues();
+ this.updateIssueStateTabs();
+ },
+ issues() {
+ this.setupExternalEvents();
+ },
+ },
+ mounted() {
+ this.loadIssues();
+ this.updateIssueStateTabs();
+ this.setupExternalEvents();
+
+ if (this.isInGroupsPage || this.isInDashboardPage) {
+ this.projectSelect();
+ }
+ },
+ updated() {
+ this.setupExternalEvents();
+ },
+ methods: {
+ ...mapActions('issuesList', ['fetchIssues', 'setCurrentPage']),
+ getCurrentState() {
+ const { state } = urlParamsToObject(this.appliedFilters);
+ return state || ISSUE_STATES.OPENED;
+ },
+ updateIssueStateTabs() {
+ const activeTabEl = document.querySelector('.issues-state-filters .active');
+ const newActiveTabEl = document.querySelector(
+ `.issues-state-filters [data-state="${this.getCurrentState()}"]`,
+ );
+
+ if (activeTabEl && !activeTabEl.querySelector(`[data-state="${this.getCurrentState()}"]`)) {
+ activeTabEl.classList.remove(ACTIVE_TAB_CLASS);
+ newActiveTabEl.parentElement.classList.add(ACTIVE_TAB_CLASS);
+ } else if (newActiveTabEl) {
+ newActiveTabEl.parentElement.classList.add(ACTIVE_TAB_CLASS);
+ }
+ },
+ setupExternalEvents() {
+ if (this.isInProjectPage && this.issuableIndex.bulkUpdateSidebar) {
+ this.issuableIndex.bulkUpdateSidebar.initDomElements();
+ this.issuableIndex.bulkUpdateSidebar.bindEvents();
+ }
+ },
+ updatePage(page) {
+ this.filteredSearch.updateObject(mergeUrlParams({ page }, this.appliedFilters));
+ this.setCurrentPage(page);
+ scrollToElement('#content-body');
+ },
+ loadIssues() {
+ if (!this.isInDashboardPage) {
+ this.isLoadingDisabled = false;
+ } else {
+ const assigneeUsername = urlParamsToObject(this.appliedFilters).assignee_username;
+ this.isLoadingDisabled = assigneeUsername !== gon.current_username;
+ }
+
+ if (!this.isLoadingDisabled) {
+ this.fetchIssues(this.endpoint);
+ }
+ },
+ applyLabelFilter(label) {
+ this.filteredSearch.clearSearch();
+ this.filteredSearch.updateObject(
+ `?${objectToQueryString({ 'label_name[]': encodeURIComponent(label) })}`,
+ );
+ this.filteredSearch.loadSearchParamsFromURL();
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="hasIssues">
+ <ul class="content-list issues-list issuable-list">
+ <issue
+ v-for="issue in issues"
+ :key="issue.id"
+ :issue="issue"
+ :is-bulk-updating="isBulkUpdating"
+ :can-bulk-update="canBulkUpdate"
+ @issueLabelClicked="applyLabelFilter"
+ />
+ </ul>
+ <div class="gl-pagination prepend-top-default">
+ <gl-pagination
+ :change="updatePage"
+ :page="currentPage"
+ :per-page="ISSUES_PER_PAGE"
+ :total-items="totalItems"
+ :next-text="__('Next')"
+ :prev-text="__('Prev')"
+ class="justify-content-center"
+ />
+ </div>
+ </div>
+ <IssuesLoadingState v-else-if="loading" />
+ <issues-empty-state
+ v-else
+ :state="getCurrentState()"
+ :button-path="createPath"
+ :has-filters="hasFilters"
+ :loading-disabled="isLoadingDisabled"
+ :svg-image-path="emptyStateSvgPath"
+ :svg-loading-disabled-image-path="emptyStateLoadingDisabledSvgPath"
+ />
+</template>
diff --git a/app/assets/javascripts/issues/components/loading_state.vue b/app/assets/javascripts/issues/components/loading_state.vue
new file mode 100644
index 00000000000..85ccaf75792
--- /dev/null
+++ b/app/assets/javascripts/issues/components/loading_state.vue
@@ -0,0 +1,17 @@
+<script>
+import { GlSkeletonLoading } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoading,
+ },
+};
+</script>
+
+<template>
+ <ul class="content-list issues-list issuable-list js-issues-loading">
+ <li v-for="n in 8" :key="n" class="issue">
+ <gl-skeleton-loading />
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
new file mode 100644
index 00000000000..6e7d6e525fb
--- /dev/null
+++ b/app/assets/javascripts/issues/constants.js
@@ -0,0 +1,9 @@
+export const ISSUE_STATES = {
+ OPENED: 'opened',
+ CLOSED: 'closed',
+};
+
+export const ACTIVE_TAB_CLASS = 'active';
+export const ISSUES_PER_PAGE = 20;
+export const DASHBOARD_PAGE_NAME = 'dashboard';
+export const MAX_ASSIGNEES_RENDER = 3;
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
new file mode 100644
index 00000000000..fd8c6bca66b
--- /dev/null
+++ b/app/assets/javascripts/issues/index.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import projectSelect from '~/project_select';
+import IssuableIndex from '~/issuable_index';
+import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import store from './stores';
+import IssuesApp from './components/issues_app.vue';
+import IssuesFilteredSearch from './issues_filtered_search';
+
+export default () => {
+ const el = document.querySelector('#js-issues-list');
+
+ if (!el) return null;
+
+ const { endpoint, canUpdate, createPath } = el.dataset;
+ const canBulkUpdate = Boolean(canUpdate);
+
+ // Set default filters from URL
+ store.dispatch('issuesList/setFilters', window.location.search);
+
+ // Setup filterd search component
+ const filteredSearch = new IssuesFilteredSearch(store.state.issuesList.filters);
+
+ // Setup issue page handlers
+ const issuableIndex = new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+
+ return new Vue({
+ el,
+ store,
+ components: {
+ IssuesApp,
+ },
+ mounted() {
+ filteredSearch.setup();
+ },
+ created() {
+ this.dataset = this.$options.el.dataset;
+ },
+ render(createElement) {
+ return createElement('issues-app', {
+ props: {
+ endpoint,
+ createPath,
+ projectSelect,
+ canBulkUpdate,
+ issuableIndex,
+ filteredSearch,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ emptyStateLoadingDisabledSvgPath: this.dataset.emptyStateLoadingDisabledSvgPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/issues/issues_filtered_search.js b/app/assets/javascripts/issues/issues_filtered_search.js
new file mode 100644
index 00000000000..aca98f30c84
--- /dev/null
+++ b/app/assets/javascripts/issues/issues_filtered_search.js
@@ -0,0 +1,32 @@
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import { FILTERED_SEARCH } from '~/pages/constants';
+import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
+import { historyPushState, getPagePath } from '~/lib/utils/common_utils';
+import { DASHBOARD_PAGE_NAME } from './constants';
+import issuesListStore from './stores';
+
+const isInDashboardPage = getPagePath() === DASHBOARD_PAGE_NAME;
+
+if (!isInDashboardPage) {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+}
+
+export default class FilteredSearchIssueAnalytics extends FilteredSearchManager {
+ constructor() {
+ super({
+ page: FILTERED_SEARCH.ISSUES,
+ isGroup: !isInDashboardPage,
+ isGroupDecendent: !isInDashboardPage,
+ IssuableFilteredSearchTokenKeys,
+ });
+
+ this.isHandledAsync = true;
+ }
+
+ updateObject = path => {
+ historyPushState(path);
+
+ issuesListStore.dispatch('issuesList/setFilters', path);
+ issuesListStore.dispatch('issuesList/setCurrentPage', 1);
+ };
+}
diff --git a/app/assets/javascripts/issues/services/issues_service.js b/app/assets/javascripts/issues/services/issues_service.js
new file mode 100644
index 00000000000..5f272833a7d
--- /dev/null
+++ b/app/assets/javascripts/issues/services/issues_service.js
@@ -0,0 +1,13 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ fetchIssues(endpoint, filters, state) {
+ return axios.get(`${endpoint}`, {
+ params: {
+ ...filters,
+ state,
+ with_labels_details: true, // ensures we more details for labels
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/issues/stores/index.js b/app/assets/javascripts/issues/stores/index.js
new file mode 100644
index 00000000000..4b437f377ba
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import issuesList from './modules/issues_list';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ modules: {
+ issuesList: issuesList(),
+ },
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/actions.js b/app/assets/javascripts/issues/stores/modules/issues_list/actions.js
new file mode 100644
index 00000000000..321e1e8d341
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/actions.js
@@ -0,0 +1,105 @@
+import flash from '~/flash';
+import { __ } from '~/locale';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import _ from 'underscore';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import { ISSUE_STATES } from '../../../constants';
+import service from '../../../services/issues_service';
+import * as types from './mutation_types';
+
+/**
+ * Return a object containg order and sort values
+ * for issues filter. This is necessary to maintain
+ * URL compatibility for current issues filters while
+ * keeping backward compatibility for issues API
+ * @param {String} sort
+ */
+const transformSortFilter = sort => {
+ const orderMap = {
+ created: 'created_at',
+ updated: 'updated_at',
+ due_date: 'due_date',
+ priority: 'priority',
+ milestone: 'milestone',
+ popularity: 'popularity',
+ label_priority: 'label_priority',
+ };
+
+ if (!sort) {
+ return null;
+ }
+
+ if (sort.lastIndexOf('desc') === -1) {
+ return {
+ orderBy: orderMap[sort],
+ sort: 'asc',
+ };
+ }
+
+ const splitIndex = sort.lastIndexOf('_');
+ const orderValue = sort.slice(0, splitIndex);
+ const sortValue = sort.slice(splitIndex + 1);
+
+ return {
+ orderBy: orderMap[orderValue],
+ sort: sortValue,
+ };
+};
+
+export const setFilters = ({ commit }, value) => {
+ commit(types.SET_FILTERS, value);
+};
+
+export const setLoadingState = ({ commit }, value) => {
+ commit(types.SET_LOADING_STATE, value);
+};
+
+export const setBulkUpdateState = ({ commit }, value) => {
+ commit(types.SET_BULK_UPDATE_STATE, value);
+};
+
+export const fetchIssues = ({ commit, dispatch, getters }, endpoint) => {
+ dispatch('setLoadingState', true);
+
+ // we always update state from the window.location
+ // as it may not be avaliable in our store
+ const [currentState] = getParameterValues('state');
+
+ // only set state if it does not exist
+ const state = !currentState ? ISSUE_STATES.OPENED : null;
+ const appliedFilters = urlParamsToObject(getters.appliedFilters);
+ const filters = _.omit(appliedFilters, ['label_name', 'sort']);
+
+ // map label parameter to supported API value
+ if (appliedFilters.label_name && Array.isArray(appliedFilters.label_name)) {
+ filters.labels = appliedFilters.label_name.join(',');
+ }
+
+ // transform and apply order_by and sort filters
+ if (appliedFilters.sort) {
+ const { sort, orderBy } = transformSortFilter(appliedFilters.sort) || {};
+ filters.sort = sort;
+ filters.order_by = orderBy;
+ }
+
+ return service
+ .fetchIssues(endpoint, filters, state)
+ .then(({ data, headers }) => {
+ dispatch('setTotalItems', headers['x-total']);
+ dispatch('setCurrentPage', headers['x-page']);
+ commit(types.SET_ISSUES_DATA, data);
+ })
+ .then(() => dispatch('setLoadingState', false))
+ .catch(() => flash(__('An error occurred while loading issues')));
+};
+
+export const setCurrentPage = ({ commit }, value) => {
+ commit(types.SET_CURRENT_PAGE, value);
+};
+
+export const setTotalItems = ({ commit }, value) => {
+ commit(types.SET_TOTAL_ITEMS, value);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/getters.js b/app/assets/javascripts/issues/stores/modules/issues_list/getters.js
new file mode 100644
index 00000000000..31c96913b85
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/getters.js
@@ -0,0 +1,29 @@
+import { tokenKeys } from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+
+const filterTokenKeys = tokenKeys.map(token => {
+ const { key, param } = token;
+ const paramValue = param ? `_${param.replace('[]', '')}` : '';
+
+ return `${key}${paramValue}`;
+});
+
+export const hasFilters = state => {
+ if (!state.filters) {
+ return false;
+ }
+
+ const currenFilters = Object.keys(urlParamsToObject(state.filters));
+
+ return currenFilters.reduce((acc, val) => {
+ if (!acc) {
+ return filterTokenKeys.includes(val);
+ }
+ return acc;
+ }, false);
+};
+export const appliedFilters = state => state.filters;
+export const currentPage = state => state.currentPage;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/index.js b/app/assets/javascripts/issues/stores/modules/issues_list/index.js
new file mode 100644
index 00000000000..81dab0566c1
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import mutations from './mutations';
+import * as actions from './actions';
+import * as getters from './getters';
+
+export default () => ({
+ namespaced: true,
+ state: state(),
+ mutations,
+ actions,
+ getters,
+});
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/mutation_types.js b/app/assets/javascripts/issues/stores/modules/issues_list/mutation_types.js
new file mode 100644
index 00000000000..df2f8cf6e37
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/mutation_types.js
@@ -0,0 +1,6 @@
+export const SET_FILTERS = 'SET_FILTERS';
+export const SET_ISSUES_DATA = 'SET_ISSUES_DATA';
+export const SET_TOTAL_ITEMS = 'SET_TOTAL_ITEMS';
+export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
+export const SET_LOADING_STATE = 'SET_LOADING_STATE';
+export const SET_BULK_UPDATE_STATE = 'SET_BULK_UPDATE_STATE';
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/mutations.js b/app/assets/javascripts/issues/stores/modules/issues_list/mutations.js
new file mode 100644
index 00000000000..0ff879b7ef7
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/mutations.js
@@ -0,0 +1,24 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_LOADING_STATE](state, value) {
+ state.loading = value;
+ },
+ [types.SET_ISSUES_DATA](state, issues) {
+ Object.assign(state, {
+ issues,
+ });
+ },
+ [types.SET_FILTERS](state, value) {
+ state.filters = value;
+ },
+ [types.SET_BULK_UPDATE_STATE](state, value) {
+ state.isBulkUpdating = value;
+ },
+ [types.SET_TOTAL_ITEMS](state, value) {
+ state.totalItems = parseInt(value, 10);
+ },
+ [types.SET_CURRENT_PAGE](state, value) {
+ state.currentPage = parseInt(value, 10);
+ },
+};
diff --git a/app/assets/javascripts/issues/stores/modules/issues_list/state.js b/app/assets/javascripts/issues/stores/modules/issues_list/state.js
new file mode 100644
index 00000000000..ffa5bd49c26
--- /dev/null
+++ b/app/assets/javascripts/issues/stores/modules/issues_list/state.js
@@ -0,0 +1,12 @@
+import { getParameterValues } from '~/lib/utils/url_utility';
+
+const [page] = getParameterValues('page');
+
+export default () => ({
+ loading: false,
+ filters: '',
+ issues: null,
+ isBulkUpdating: false,
+ currentPage: page || 1,
+ totalItems: 0,
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f0254be2044..0d1acf0544d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1080,6 +1080,9 @@ msgstr ""
msgid "An error occurred while loading filenames"
msgstr ""
+msgid "An error occurred while loading issues"
+msgstr ""
+
msgid "An error occurred while loading the file"
msgstr ""
@@ -1945,6 +1948,9 @@ msgstr ""
msgid "CICD|instance enabled"
msgstr ""
+msgid "CLOSED"
+msgstr ""
+
msgid "CONTRIBUTING"
msgstr ""
@@ -8063,6 +8069,9 @@ msgstr ""
msgid "Press Enter or click to search"
msgstr ""
+msgid "Prev"
+msgstr ""
+
msgid "Preview"
msgstr ""
@@ -12485,6 +12494,9 @@ msgstr ""
msgid "Wednesday"
msgstr ""
+msgid "Weight"
+msgstr ""
+
msgid "Welcome to your Issue Board!"
msgstr ""
@@ -13601,6 +13613,9 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}"
msgstr ""
+msgid "opened"
+msgstr ""
+
msgid "or"
msgstr ""
diff --git a/spec/frontend/helpers/url_util_helper.js b/spec/frontend/helpers/url_util_helper.js
new file mode 100644
index 00000000000..d28e9aa7bf9
--- /dev/null
+++ b/spec/frontend/helpers/url_util_helper.js
@@ -0,0 +1,7 @@
+// eslint-disable-next-line import/prefer-default-export
+export const setWindowLocation = value => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value,
+ });
+};
diff --git a/spec/frontend/issues/components/issue_spec.js b/spec/frontend/issues/components/issue_spec.js
new file mode 100644
index 00000000000..66252ad4d97
--- /dev/null
+++ b/spec/frontend/issues/components/issue_spec.js
@@ -0,0 +1,222 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import IssueComponent from '~/issues/components/issue.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { singleIssueData } from '../mock_data';
+
+const localVue = createLocalVue();
+
+describe('Issue component', () => {
+ let wrapper;
+
+ const factory = (props = {}, options = {}) => {
+ const propsData = {
+ issue: singleIssueData,
+ isBulkUpdating: false,
+ canBulkUpdate: true,
+ ...props,
+ };
+
+ wrapper = shallowMount(localVue.extend(IssueComponent), {
+ localVue,
+ propsData,
+ ...options,
+ });
+ };
+
+ it('renders the issue title', () => {
+ factory();
+
+ const issueTitle = wrapper.find('.issue-title-text > a');
+
+ expect(issueTitle.exists()).toBe(true);
+ expect(issueTitle.text()).toBe(singleIssueData.title);
+ });
+
+ it('links to the issue details page', () => {
+ factory();
+
+ const issueTitle = wrapper.find('.issue-title-text > a');
+
+ expect(issueTitle.attributes('href')).toBe(singleIssueData.web_url);
+ });
+
+ it('renders issue confidential icon', () => {
+ factory();
+
+ const confidentialIcon = wrapper.find('.js-issue-confidential-icon');
+
+ expect(confidentialIcon.exists()).toBe(true);
+ });
+
+ it('renders issue tasks', () => {
+ const issue = { ...singleIssueData };
+ issue.has_tasks = true;
+ issue.task_status = '0 or 2 completed';
+
+ factory({ issue });
+
+ const issueTasksWrapper = wrapper.find('.task-status');
+
+ expect(issueTasksWrapper.exists()).toBe(true);
+ expect(issueTasksWrapper.text()).toBe(issue.task_status);
+ });
+
+ it('renders issue reference path', () => {
+ factory();
+
+ const referencePath = wrapper.find('.issuable-reference');
+
+ expect(referencePath.exists()).toBe(true);
+ expect(referencePath.text()).toBe(singleIssueData.reference_path);
+ });
+
+ it('renders issue created date', () => {
+ factory();
+
+ const issueCreatedDate = wrapper.find('.issuable-authored').find(TimeAgoTooltip);
+
+ expect(issueCreatedDate.exists()).toBe(true);
+ expect(issueCreatedDate.props('time')).toBe(singleIssueData.created_at);
+ });
+
+ it('renders issue author', () => {
+ factory();
+
+ const issueAuthor = wrapper.find('.issuable-authored .author-link');
+
+ expect(issueAuthor.exists()).toBe(true);
+ expect(issueAuthor.text()).toBe(singleIssueData.author.name);
+ expect(issueAuthor.attributes('href')).toBe(singleIssueData.author.web_url);
+ });
+
+ it('renders issue milestone', () => {
+ const issue = { ...singleIssueData };
+ issue.milestone = {
+ web_url: 'http://hello.world',
+ title: 'Hello milestone',
+ };
+
+ factory({ issue });
+
+ const issueMilestone = wrapper.find('.issuable-milestone');
+
+ expect(issueMilestone.exists()).toBe(true);
+ expect(issueMilestone.text()).toBe(issue.milestone.title);
+ expect(issueMilestone.find('a').attributes('href')).toBe(issue.milestone.web_url);
+ });
+
+ it('renders issue weight', () => {
+ factory();
+
+ const issueWeight = wrapper.find('.issuable-weight');
+
+ expect(issueWeight.exists()).toBe(true);
+ expect(issueWeight.text()).toBe(`${singleIssueData.weight}`);
+ });
+
+ it('renders issue "CLOSED" if issue is closed', () => {
+ const issue = { ...singleIssueData };
+ issue.state = 'closed';
+
+ factory({ issue });
+
+ const issueStateWrapper = wrapper.find('.issuable-status');
+
+ expect(issueStateWrapper.exists()).toBe(true);
+ expect(issueStateWrapper.text()).toBe('CLOSED');
+ });
+
+ it('renders issue merge request count', () => {
+ const issue = { ...singleIssueData };
+ issue.merge_requests_count = '20';
+
+ factory({ issue });
+
+ const issueMRCount = wrapper.find('.issuable-mr');
+
+ expect(issueMRCount.exists()).toBe(true);
+ expect(issueMRCount.text()).toBe(issue.merge_requests_count);
+ });
+
+ describe('Issue upvotes', () => {
+ let issue;
+
+ beforeEach(() => {
+ issue = { ...singleIssueData };
+ issue.upvotes = '5';
+ });
+
+ it('does not render if upvotes is 0', () => {
+ issue.upvotes = 0;
+
+ factory({ issue });
+
+ const issueUpvotes = wrapper.find('.issuable-upvotes');
+
+ expect(issueUpvotes.exists()).toBe(false);
+ });
+
+ it('renders upvotes count', () => {
+ factory({ issue });
+
+ const issueUpvotes = wrapper.find('.issuable-upvotes');
+
+ expect(issueUpvotes.exists()).toBe(true);
+ expect(issueUpvotes.text()).toBe(issue.upvotes);
+ });
+ });
+
+ describe('Issue downvotes', () => {
+ let issue;
+
+ beforeEach(() => {
+ issue = { ...singleIssueData };
+ issue.downvotes = '5';
+ });
+
+ it('does not render if downvotes is 0', () => {
+ issue.downvotes = 0;
+
+ factory({ issue });
+
+ const issueDownvotes = wrapper.find('.issuable-downvotes');
+
+ expect(issueDownvotes.exists()).toBe(false);
+ });
+
+ it('renders downvotes count', () => {
+ factory({ issue });
+
+ const issueDownvotes = wrapper.find('.issuable-downvotes');
+
+ expect(issueDownvotes.exists()).toBe(true);
+ expect(issueDownvotes.text()).toBe(issue.downvotes);
+ });
+ });
+
+ describe('Issue comments', () => {
+ let issue;
+
+ beforeEach(() => {
+ issue = { ...singleIssueData };
+ issue.note_count = '5';
+ });
+
+ it('renders 0 if the issue has no comments', () => {
+ issue.note_count = null;
+
+ factory({ issue });
+
+ const issueComments = wrapper.find('.issuable-comments');
+
+ expect(issueComments.text()).toBe('0');
+ });
+
+ it('renders issue comments count', () => {
+ factory({ issue });
+
+ const issueComments = wrapper.find('.issuable-comments');
+ expect(issueComments.text()).toBe(issue.note_count);
+ });
+ });
+});
diff --git a/spec/frontend/issues/components/issues_app_spec.js b/spec/frontend/issues/components/issues_app_spec.js
new file mode 100644
index 00000000000..b8dcf72fa43
--- /dev/null
+++ b/spec/frontend/issues/components/issues_app_spec.js
@@ -0,0 +1,234 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import { GlPagination } from '@gitlab/ui';
+import IssuesApp from '~/issues/components/issues_app.vue';
+import IssueComponent from '~/issues/components/issue.vue';
+import IssuesEmptyState from '~/issues/components/empty_state.vue';
+import IssuesLoadingState from '~/issues/components/loading_state.vue';
+import * as getters from '~/issues/stores/modules/issues_list/getters';
+import { issuesResponseData } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Issues app', () => {
+ let store;
+ let state;
+ let actions;
+ let wrapper;
+ let mockedProjectSelect;
+ let mockedIssuableIndex;
+ let mockedFilteredSearch;
+
+ const factory = (props = {}, options = {}) => {
+ const propsData = {
+ endpoint: TEST_HOST,
+ createPath: '/',
+ projectSelect: mockedProjectSelect,
+ canBulkUpdate: true,
+ issuableIndex: mockedIssuableIndex,
+ filteredSearch: mockedFilteredSearch,
+ emptyStateSvgPath: '/',
+ emptyStateLoadingDisabledSvgPath: '/',
+ ...props,
+ };
+
+ store = new Vuex.Store({
+ modules: {
+ issuesList: {
+ namespaced: true,
+ state,
+ actions,
+ getters,
+ },
+ },
+ });
+
+ wrapper = mount(localVue.extend(IssuesApp), {
+ localVue,
+ store,
+ sync: false,
+ propsData,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ state = {
+ loading: false,
+ filters: '',
+ issues: null,
+ isBulkUpdating: false,
+ currentPage: 1,
+ totalItems: 1,
+ };
+
+ actions = {
+ fetchIssues: jest.fn(),
+ setCurrentPage: jest.fn(),
+ };
+
+ mockedFilteredSearch = {
+ updateObject: jest.fn(),
+ clearSearch: jest.fn(),
+ loadSearchParamsFromURL: jest.fn(),
+ };
+
+ mockedIssuableIndex = {
+ bulkUpdateSidebar: {
+ bindEvents: jest.fn(),
+ initDomElements: jest.fn(),
+ },
+ };
+
+ mockedProjectSelect = jest.fn();
+
+ document.body.innerHTML = '';
+ window.gon = {};
+ });
+
+ it('fetches issues when mounted', () => {
+ factory();
+ expect(actions.fetchIssues).toHaveBeenCalled();
+ expect(actions.fetchIssues.mock.calls[0]).toContain(TEST_HOST);
+ });
+
+ it('renders loading state when fetching issues', () => {
+ state.loading = true;
+ factory();
+
+ const loadingState = wrapper.find(IssuesLoadingState);
+
+ expect(loadingState.exists()).toBe(true);
+ expect(loadingState.classes()).toContain('js-issues-loading');
+ });
+
+ it('renders empty state if no issues', () => {
+ state.issues = [];
+ factory();
+
+ const emptyState = wrapper.find(IssuesEmptyState);
+
+ expect(emptyState.exists()).toBe(true);
+ });
+
+ it('renders issues list when issues are present', () => {
+ state.issues = issuesResponseData.slice(0, 10);
+ factory();
+
+ const issueComponent = wrapper.find(IssueComponent);
+
+ expect(issueComponent.exists()).toBe(true);
+ expect(wrapper.findAll(IssueComponent).length).toEqual(10);
+ });
+
+ it('renders issues list pagination', () => {
+ state.issues = issuesResponseData;
+ factory();
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
+ });
+
+ it('initializes issues page events', () => {
+ state.issues = issuesResponseData;
+ factory();
+
+ wrapper.setData({ isInProjectPage: true });
+ wrapper.vm.setupExternalEvents();
+
+ expect(mockedIssuableIndex.bulkUpdateSidebar.bindEvents).toHaveBeenCalled();
+ expect(mockedIssuableIndex.bulkUpdateSidebar.initDomElements).toHaveBeenCalled();
+ });
+
+ it('initializes project selector in issues dashboard page', () => {
+ state.issues = issuesResponseData;
+ factory(
+ {},
+ {
+ data() {
+ return {
+ ISSUES_PER_PAGE: 20,
+ isInGroupsPage: false,
+ isInProjectPage: false,
+ isInDashboardPage: true,
+ isLoadingDisabled: false,
+ };
+ },
+ },
+ );
+
+ expect(mockedProjectSelect).toHaveBeenCalled();
+ });
+
+ it('initializes project selector in groups issues page', () => {
+ state.issues = issuesResponseData;
+ factory(
+ {},
+ {
+ data() {
+ return {
+ ISSUES_PER_PAGE: 20,
+ isInGroupsPage: true,
+ isInProjectPage: false,
+ isInDashboardPage: false,
+ isLoadingDisabled: false,
+ };
+ },
+ },
+ );
+
+ expect(mockedProjectSelect).toHaveBeenCalled();
+ });
+
+ describe('Issues filters', () => {
+ it('updates filters when the current page changes', () => {
+ const page = 2;
+
+ document.body.innerHTML = '<div id="content-body"></div>';
+ state.issues = issuesResponseData;
+ factory();
+
+ wrapper.vm.updatePage(page);
+
+ expect(mockedFilteredSearch.updateObject).toHaveBeenCalled();
+ expect(actions.setCurrentPage.mock.calls[0]).toContain(page);
+ });
+
+ it('update filters when a label is clicked', () => {
+ const label = 'Hello';
+ state.issues = issuesResponseData;
+ factory();
+
+ wrapper.vm.applyLabelFilter(label);
+
+ expect(mockedFilteredSearch.clearSearch).toHaveBeenCalled();
+ expect(mockedFilteredSearch.updateObject).toHaveBeenCalled();
+ expect(mockedFilteredSearch.loadSearchParamsFromURL).toHaveBeenCalled();
+ });
+
+ it('disables loading in issues dashboard page if assignee filter is incorrect', () => {
+ window.gon = {
+ current_username: 'not_root',
+ };
+
+ state.issues = [];
+ state.filters = '?assignee_username=root';
+ factory(
+ {},
+ {
+ data() {
+ return {
+ ISSUES_PER_PAGE: 20,
+ isInGroupsPage: false,
+ isInProjectPage: false,
+ isInDashboardPage: true,
+ isLoadingDisabled: false,
+ };
+ },
+ },
+ );
+
+ expect(wrapper.vm.isLoadingDisabled).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/issues/mock_data.js b/spec/frontend/issues/mock_data.js
new file mode 100644
index 00000000000..2a0f209336d
--- /dev/null
+++ b/spec/frontend/issues/mock_data.js
@@ -0,0 +1,1491 @@
+export const issuesResponseData = [
+ {
+ id: 389,
+ iid: 1,
+ project_id: 15,
+ title: 'Add Content',
+ description: 'Add content to this repo',
+ state: 'opened',
+ created_at: '2019-04-06T11:06:27.510Z',
+ updated_at: '2019-04-06T11:06:27.510Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: null,
+ assignees: [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ ],
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ assignee: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: '2019-07-25',
+ confidential: true,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/awesome-jekyll/issues/1',
+ reference_path: '#1',
+ real_path: '/gitlab-org/awesome-jekyll/issues/1',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: 20,
+ },
+ {
+ id: 246,
+ iid: 33,
+ project_id: 1,
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ state: 'opened',
+ created_at: '2018-11-05T09:26:15.164Z',
+ updated_at: '2019-04-11T20:07:02.662Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: null,
+ assignees: [],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: null,
+ user_notes_count: 2,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/33',
+ reference_path: '#33',
+ real_path: '/gitlab-org/gitlab-test/issues/33',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 245,
+ iid: 32,
+ project_id: 1,
+ title: 'Dismiss Cipher with no integrity',
+ description:
+ '* [ ] Do the first thing\n* [ ] Do the next thing\n* [ ] Do the third thing\n* [ ] Do the last thing',
+ state: 'opened',
+ created_at: '2018-11-05T09:26:15.103Z',
+ updated_at: '2019-04-12T05:43:47.971Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ {
+ id: 38,
+ name: 'priority::1',
+ color: '#FF0000',
+ description: 'This is for top level issues that should we worked on first',
+ text_color: '#FFFFFF',
+ },
+ ],
+ milestone: null,
+ assignees: [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ {
+ id: 13,
+ name: 'Shannan Quigley',
+ username: 'alva_cassin',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/8456544bf25a412a9c08f7b6c4e7fc62?s=80\u0026d=identicon',
+ web_url: 'http://test.host/alva_cassin',
+ },
+ {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ ],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ user_notes_count: 3,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: true,
+ discussion_locked: null,
+ has_tasks: true,
+ task_status: '0 of 4 tasks completed',
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/32',
+ reference_path: '#32',
+ real_path: '/gitlab-org/gitlab-test/issues/32',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 244,
+ iid: 31,
+ project_id: 1,
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ state: 'opened',
+ created_at: '2018-11-05T09:26:15.047Z',
+ updated_at: '2019-04-11T20:07:02.542Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: null,
+ assignees: [],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: null,
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/31',
+ reference_path: '#31',
+ real_path: '/gitlab-org/gitlab-test/issues/31',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 20,
+ iid: 10,
+ project_id: 2,
+ title: 'Quo beatae ullam laborum est quisquam laudantium ut et autem mollitia.',
+ description: 'Eveniet voluptatem molestiae dolor expedita.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:13.533Z',
+ updated_at: '2019-03-29T08:32:36.294Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 6,
+ iid: 1,
+ project_id: 2,
+ title: 'v0.0',
+ description: 'Non nemo aut consequatur et recusandae.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.485Z',
+ updated_at: '2018-11-05T09:21:09.485Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/1',
+ },
+ assignees: [
+ {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ ],
+ author: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ assignee: {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ user_notes_count: 12,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/10',
+ reference_path: '#10',
+ real_path: '/gitlab-org/gitlab-shell/issues/10',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 19,
+ iid: 9,
+ project_id: 2,
+ title: 'Aliquam earum sit dolore voluptates assumenda autem omnis eum necessitatibus minus.',
+ description: 'Et qui atque deleniti nulla non sed provident ut.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:13.428Z',
+ updated_at: '2019-01-30T07:17:54.796Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 8,
+ iid: 3,
+ project_id: 2,
+ title: 'v2.0',
+ description: 'Debitis sit aut rerum voluptatem provident corporis sint.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.524Z',
+ updated_at: '2018-11-05T09:21:09.524Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/3',
+ },
+ assignees: [
+ {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ ],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/9',
+ reference_path: '#9',
+ real_path: '/gitlab-org/gitlab-shell/issues/9',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 17,
+ iid: 7,
+ project_id: 2,
+ title: 'Est nihil ut aut et excepturi ut.',
+ description: 'Est nesciunt dolor et similique porro ipsam libero.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:13.207Z',
+ updated_at: '2019-01-22T16:04:41.645Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 10,
+ iid: 5,
+ project_id: 2,
+ title: 'v4.0',
+ description: 'Ipsam amet possimus cupiditate fugit perferendis non enim eligendi.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.562Z',
+ updated_at: '2018-11-05T09:21:09.562Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/5',
+ },
+ assignees: [
+ {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ ],
+ author: {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ assignee: {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ user_notes_count: 7,
+ merge_requests_count: 1,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: '2018-11-09',
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/7',
+ reference_path: '#7',
+ real_path: '/gitlab-org/gitlab-shell/issues/7',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 16,
+ iid: 6,
+ project_id: 2,
+ title: 'Qui fuga esse explicabo facilis velit ad eius error mollitia sed.',
+ description: 'Architecto sed alias cum non incidunt et nemo minus.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:13.105Z',
+ updated_at: '2018-11-05T09:22:22.510Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 9,
+ iid: 4,
+ project_id: 2,
+ title: 'v3.0',
+ description: 'Omnis et non explicabo nostrum velit nihil.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.545Z',
+ updated_at: '2018-11-05T09:21:09.545Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/4',
+ },
+ assignees: [
+ {
+ id: 14,
+ name: 'Shantell Stokes',
+ username: 'chi',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/a21b34070d1abbae545f302caeaca174?s=80\u0026d=identicon',
+ web_url: 'http://test.host/chi',
+ },
+ ],
+ author: {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ assignee: {
+ id: 14,
+ name: 'Shantell Stokes',
+ username: 'chi',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/a21b34070d1abbae545f302caeaca174?s=80\u0026d=identicon',
+ web_url: 'http://test.host/chi',
+ },
+ user_notes_count: 7,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/6',
+ reference_path: '#6',
+ real_path: '/gitlab-org/gitlab-shell/issues/6',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 15,
+ iid: 5,
+ project_id: 2,
+ title: 'Corrupti numquam in quibusdam sunt quia autem quam suscipit itaque.',
+ description: 'Vel officia esse quasi sunt.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:13.000Z',
+ updated_at: '2018-11-05T09:22:22.250Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 9,
+ iid: 4,
+ project_id: 2,
+ title: 'v3.0',
+ description: 'Omnis et non explicabo nostrum velit nihil.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.545Z',
+ updated_at: '2018-11-05T09:21:09.545Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/4',
+ },
+ assignees: [
+ {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ ],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ user_notes_count: 7,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/5',
+ reference_path: '#5',
+ real_path: '/gitlab-org/gitlab-shell/issues/5',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 12,
+ iid: 2,
+ project_id: 2,
+ title: 'Consequatur autem dolor facere architecto esse et delectus.',
+ description: 'Corporis esse ea iste incidunt porro ex qui.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:12.702Z',
+ updated_at: '2018-11-05T09:22:21.414Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 6,
+ iid: 1,
+ project_id: 2,
+ title: 'v0.0',
+ description: 'Non nemo aut consequatur et recusandae.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.485Z',
+ updated_at: '2018-11-05T09:21:09.485Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/1',
+ },
+ assignees: [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ ],
+ author: {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ assignee: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ user_notes_count: 7,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/2',
+ reference_path: '#2',
+ real_path: '/gitlab-org/gitlab-shell/issues/2',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 10,
+ iid: 10,
+ project_id: 1,
+ title: 'Asperiores rerum a ea ipsam cum explicabo aperiam deleniti et officia.',
+ description: 'Aperiam eum explicabo doloribus molestias nostrum ex labore consequatur.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:12.439Z',
+ updated_at: '2018-11-05T09:22:20.837Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 1,
+ iid: 1,
+ project_id: 1,
+ title: 'v0.0',
+ description: 'Eum laborum aut facilis qui voluptas est.',
+ state: 'closed',
+ created_at: '2018-11-05T09:21:09.381Z',
+ updated_at: '2018-11-05T09:21:09.381Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/1',
+ },
+ assignees: [
+ {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ ],
+ author: {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ assignee: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/10',
+ reference_path: '#10',
+ real_path: '/gitlab-org/gitlab-test/issues/10',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 8,
+ iid: 8,
+ project_id: 1,
+ title: 'Nihil inventore fugiat quibusdam dignissimos aperiam nemo ea dolor consequatur.',
+ description: 'Maxime doloremque quasi sapiente quos ex eligendi expedita id.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:12.240Z',
+ updated_at: '2019-04-11T20:07:02.265Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: {
+ id: 2,
+ iid: 2,
+ project_id: 1,
+ title: 'v1.0',
+ description: 'Ut libero impedit perferendis et voluptate eos.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.411Z',
+ updated_at: '2018-11-05T09:21:09.411Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/2',
+ },
+ assignees: [
+ {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ ],
+ author: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ assignee: {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/8',
+ reference_path: '#8',
+ real_path: '/gitlab-org/gitlab-test/issues/8',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 7,
+ iid: 7,
+ project_id: 1,
+ title: 'Sit ut ab commodi aut nulla veritatis corrupti velit pariatur sint.',
+ description: 'Reiciendis ab assumenda consequatur magni consequatur.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:12.109Z',
+ updated_at: '2019-04-11T20:07:02.115Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: {
+ id: 2,
+ iid: 2,
+ project_id: 1,
+ title: 'v1.0',
+ description: 'Ut libero impedit perferendis et voluptate eos.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.411Z',
+ updated_at: '2018-11-05T09:21:09.411Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/2',
+ },
+ assignees: [
+ {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ ],
+ author: {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ assignee: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/7',
+ reference_path: '#7',
+ real_path: '/gitlab-org/gitlab-test/issues/7',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 6,
+ iid: 6,
+ project_id: 1,
+ title: 'Sed autem non aut consequatur odio eveniet quidem omnis ea.',
+ description: 'Rerum eligendi sunt qui ducimus nihil modi quia deserunt.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:11.994Z',
+ updated_at: '2019-04-11T20:07:01.956Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: {
+ id: 2,
+ iid: 2,
+ project_id: 1,
+ title: 'v1.0',
+ description: 'Ut libero impedit perferendis et voluptate eos.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.411Z',
+ updated_at: '2018-11-05T09:21:09.411Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/2',
+ },
+ assignees: [
+ {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ ],
+ author: {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ assignee: {
+ id: 23,
+ name: 'User 1',
+ username: 'user1',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80\u0026d=identicon',
+ web_url: 'http://test.host/user1',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/6',
+ reference_path: '#6',
+ real_path: '/gitlab-org/gitlab-test/issues/6',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 5,
+ iid: 5,
+ project_id: 1,
+ title: 'Voluptas accusamus ut officiis ut voluptatem perspiciatis in eius dicta doloribus.',
+ description: 'Quibusdam cupiditate qui molestias autem aut voluptatem voluptatem mollitia.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:11.877Z',
+ updated_at: '2019-04-11T20:07:01.823Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: {
+ id: 4,
+ iid: 4,
+ project_id: 1,
+ title: 'v3.0',
+ description: 'Tempore voluptatem corrupti a perspiciatis autem.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.446Z',
+ updated_at: '2018-11-05T09:21:09.446Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/4',
+ },
+ assignees: [
+ {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ ],
+ author: {
+ id: 13,
+ name: 'Shannan Quigley',
+ username: 'alva_cassin',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/8456544bf25a412a9c08f7b6c4e7fc62?s=80\u0026d=identicon',
+ web_url: 'http://test.host/alva_cassin',
+ },
+ assignee: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/5',
+ reference_path: '#5',
+ real_path: '/gitlab-org/gitlab-test/issues/5',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 4,
+ iid: 4,
+ project_id: 1,
+ title: 'Maxime ut repudiandae voluptatem libero nostrum iste aut deleniti consectetur.',
+ description: 'Non dolorem odio sed asperiores in.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:11.768Z',
+ updated_at: '2018-11-05T09:22:19.054Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 2,
+ iid: 2,
+ project_id: 1,
+ title: 'v1.0',
+ description: 'Ut libero impedit perferendis et voluptate eos.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.411Z',
+ updated_at: '2018-11-05T09:21:09.411Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/2',
+ },
+ assignees: [
+ {
+ id: 13,
+ name: 'Shannan Quigley',
+ username: 'alva_cassin',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/8456544bf25a412a9c08f7b6c4e7fc62?s=80\u0026d=identicon',
+ web_url: 'http://test.host/alva_cassin',
+ },
+ ],
+ author: {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ assignee: {
+ id: 13,
+ name: 'Shannan Quigley',
+ username: 'alva_cassin',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/8456544bf25a412a9c08f7b6c4e7fc62?s=80\u0026d=identicon',
+ web_url: 'http://test.host/alva_cassin',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/4',
+ reference_path: '#4',
+ real_path: '/gitlab-org/gitlab-test/issues/4',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 1,
+ iid: 1,
+ project_id: 1,
+ title: 'Corrupti unde non aperiam id eligendi aut totam sit temporibus.',
+ description: 'Quia quasi dicta omnis qui.',
+ state: 'opened',
+ created_at: '2018-11-05T09:21:10.842Z',
+ updated_at: '2019-04-14T06:52:03.837Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [
+ {
+ id: 38,
+ name: 'priority::1',
+ color: '#FF0000',
+ description: 'This is for top level issues that should we worked on first',
+ text_color: '#FFFFFF',
+ },
+ { id: 36, name: 'To Do', color: '#F0AD4E', description: null, text_color: '#FFFFFF' },
+ { id: 37, name: 'Doing', color: '#5CB85C', description: null, text_color: '#FFFFFF' },
+ ],
+ milestone: {
+ id: 2,
+ iid: 2,
+ project_id: 1,
+ title: 'v1.0',
+ description: 'Ut libero impedit perferendis et voluptate eos.',
+ state: 'active',
+ created_at: '2018-11-05T09:21:09.411Z',
+ updated_at: '2018-11-05T09:21:09.411Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/2',
+ },
+ assignees: [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ ],
+ author: {
+ id: 7,
+ name: 'Minta Friesen',
+ username: 'irene_hickle',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/9231b409b04d59788caa3f1b69cab29e?s=80\u0026d=identicon',
+ web_url: 'http://test.host/irene_hickle',
+ },
+ assignee: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ user_notes_count: 8,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/1',
+ reference_path: '#1',
+ real_path: '/gitlab-org/gitlab-test/issues/1',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 152,
+ iid: 22,
+ project_id: 2,
+ title: 'Quas saepe laboriosam cupiditate incidunt eos occaecati.',
+ description: 'Quo magni blanditiis et sunt omnis dolorem enim ipsa.',
+ state: 'opened',
+ created_at: '2018-10-26T09:25:35.917Z',
+ updated_at: '2019-01-16T12:36:49.591Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 44,
+ iid: 6,
+ project_id: 2,
+ title: 'Sprint - Animi necessitatibus sit laboriosam sequi eum voluptates id illum.',
+ description: 'Architecto quod animi officia quis quas minus quidem saepe.',
+ state: 'active',
+ created_at: '2018-10-26T09:25:34.584Z',
+ updated_at: '2018-10-26T09:25:34.584Z',
+ due_date: '2018-11-03',
+ start_date: '2018-10-26',
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/6',
+ },
+ assignees: [
+ {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ ],
+ author: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ assignee: {
+ id: 20,
+ name: 'Sharee Gerhold',
+ username: 'geri_waelchi',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/7cc8a4b30b2071494223297b9aadeaec?s=80\u0026d=identicon',
+ web_url: 'http://test.host/geri_waelchi',
+ },
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/22',
+ reference_path: '#22',
+ real_path: '/gitlab-org/gitlab-shell/issues/22',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 151,
+ iid: 21,
+ project_id: 2,
+ title: 'Ad dolorem nobis et nulla tempore qui itaque provident eos et.',
+ description: 'Laboriosam enim et ipsam explicabo.',
+ state: 'opened',
+ created_at: '2018-10-26T09:25:35.807Z',
+ updated_at: '2019-01-16T12:36:49.644Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 44,
+ iid: 6,
+ project_id: 2,
+ title: 'Sprint - Animi necessitatibus sit laboriosam sequi eum voluptates id illum.',
+ description: 'Architecto quod animi officia quis quas minus quidem saepe.',
+ state: 'active',
+ created_at: '2018-10-26T09:25:34.584Z',
+ updated_at: '2018-10-26T09:25:34.584Z',
+ due_date: '2018-11-03',
+ start_date: '2018-10-26',
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/milestones/6',
+ },
+ assignees: [
+ {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ ],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: {
+ id: 10,
+ name: 'George Bartell',
+ username: 'jaimee',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b8b798fad1dd688ceab20efff7624a74?s=80\u0026d=identicon',
+ web_url: 'http://test.host/jaimee',
+ },
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-shell/issues/21',
+ reference_path: '#21',
+ real_path: '/gitlab-org/gitlab-shell/issues/21',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+ {
+ id: 119,
+ iid: 29,
+ project_id: 1,
+ title: 'Eaque minima culpa illo explicabo porro aut.',
+ description: 'Accusantium necessitatibus molestiae et quia animi.',
+ state: 'opened',
+ created_at: '2018-10-26T09:25:27.248Z',
+ updated_at: '2018-10-26T09:25:27.248Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: {
+ id: 42,
+ iid: 6,
+ project_id: 1,
+ title: 'Sprint - Delectus quasi accusamus dicta exercitationem.',
+ description: 'Corrupti recusandae in molestiae porro quae qui.',
+ state: 'active',
+ created_at: '2018-10-26T09:25:25.177Z',
+ updated_at: '2018-10-26T09:25:25.177Z',
+ due_date: '2018-11-03',
+ start_date: '2018-10-26',
+ web_url: 'http://test.host/gitlab-org/gitlab-test/milestones/6',
+ },
+ assignees: [
+ {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url:
+ 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ ],
+ author: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ assignee: {
+ id: 2,
+ name: 'Wai Hamill',
+ username: 'felicia_tremblay',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/b53d4d33fdc1d0f9296e3440e71d1bde?s=80\u0026d=identicon',
+ web_url: 'http://test.host/felicia_tremblay',
+ },
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/gitlab-test/issues/29',
+ reference_path: '#29',
+ real_path: '/gitlab-org/gitlab-test/issues/29',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: null,
+ },
+];
+
+export const singleIssueData = {
+ id: 389,
+ iid: 1,
+ project_id: 15,
+ title: 'Add Content',
+ description: 'Add content to this repo',
+ state: 'opened',
+ created_at: '2019-04-06T11:06:27.510Z',
+ updated_at: '2019-04-06T11:06:27.510Z',
+ closed_at: null,
+ closed_by: null,
+ labels: [],
+ milestone: null,
+ assignees: [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ ],
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ assignee: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'http://test.host/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://test.host/root',
+ },
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: '2019-07-25',
+ confidential: true,
+ discussion_locked: null,
+ has_tasks: false,
+ web_url: 'http://test.host/gitlab-org/awesome-jekyll/issues/1',
+ reference_path: '#1',
+ real_path: '/gitlab-org/awesome-jekyll/issues/1',
+ time_stats: {
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ },
+ weight: 20,
+};
diff --git a/spec/frontend/issues/stores/modules/issues_list/actions_spec.js b/spec/frontend/issues/stores/modules/issues_list/actions_spec.js
new file mode 100644
index 00000000000..9c5cee9d955
--- /dev/null
+++ b/spec/frontend/issues/stores/modules/issues_list/actions_spec.js
@@ -0,0 +1,111 @@
+import MockAdapter from 'axios-mock-adapter';
+import statusCodes from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+import * as issuesActions from '~/issues/stores/modules/issues_list/actions';
+import * as types from '~/issues/stores/modules/issues_list/mutation_types';
+import testAction from '../../../../helpers/vuex_action_helper';
+import { issuesResponseData } from '../../../mock_data';
+import { setWindowLocation } from '../../../../helpers/url_util_helper';
+
+describe('Issues List Actions', () => {
+ it('Should set filter value', done => {
+ const issueFilter = 'hello=world';
+
+ testAction(
+ issuesActions.setFilters,
+ issueFilter,
+ {},
+ [{ type: types.SET_FILTERS, payload: issueFilter }],
+ [],
+ done,
+ );
+ });
+
+ it('Should set loading state', done => {
+ const loadingState = 'loading';
+
+ testAction(
+ issuesActions.setLoadingState,
+ loadingState,
+ {},
+ [{ type: types.SET_LOADING_STATE, payload: loadingState }],
+ [],
+ done,
+ );
+ });
+
+ it('Should set bulk update state', done => {
+ const bulkUpdateState = 'updating';
+
+ testAction(
+ issuesActions.setBulkUpdateState,
+ bulkUpdateState,
+ {},
+ [{ type: types.SET_BULK_UPDATE_STATE, payload: bulkUpdateState }],
+ [],
+ done,
+ );
+ });
+
+ it('Should set current page', done => {
+ const currentPage = 1;
+
+ testAction(
+ issuesActions.setCurrentPage,
+ currentPage,
+ {},
+ [{ type: types.SET_CURRENT_PAGE, payload: currentPage }],
+ [],
+ done,
+ );
+ });
+
+ it('Should set total Items', done => {
+ const totalItems = 200;
+
+ testAction(
+ issuesActions.setTotalItems,
+ totalItems,
+ {},
+ [{ type: types.SET_TOTAL_ITEMS, payload: totalItems }],
+ [],
+ done,
+ );
+ });
+
+ it('should fetch issues', done => {
+ const totalIssues = 1000;
+ const currentPage = 1;
+ const issuesEndpoint = '/issues';
+ const appliedFilters = 'scope=all&utf8=%E2%9C%93&state=opened&page=2';
+ const mock = new MockAdapter(axios);
+ const { search } = window.location;
+
+ mock.onGet(issuesEndpoint).reply(statusCodes.OK, JSON.stringify({ ...issuesResponseData }), {
+ 'x-total': totalIssues,
+ 'x-page': currentPage,
+ });
+
+ setWindowLocation({
+ search: `?${appliedFilters}`,
+ });
+
+ testAction(
+ issuesActions.fetchIssues,
+ issuesEndpoint,
+ { appliedFilters },
+ [{ type: types.SET_ISSUES_DATA, payload: { ...issuesResponseData } }],
+ [
+ { type: 'setLoadingState', payload: true },
+ { type: 'setTotalItems', payload: totalIssues },
+ { type: 'setCurrentPage', payload: currentPage },
+ { type: 'setLoadingState', payload: false },
+ ],
+ () => {
+ mock.restore();
+ window.location.search = search;
+ done();
+ },
+ );
+ });
+});
diff --git a/spec/frontend/issues/stores/modules/issues_list/getters_spec.js b/spec/frontend/issues/stores/modules/issues_list/getters_spec.js
new file mode 100644
index 00000000000..f68615fdbe8
--- /dev/null
+++ b/spec/frontend/issues/stores/modules/issues_list/getters_spec.js
@@ -0,0 +1,27 @@
+import state from '~/issues/stores/modules/issues_list/state';
+import { hasFilters } from '~/issues/stores/modules/issues_list/getters';
+
+describe('Issue List Getters', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('hasFilters', () => {
+ it('should return "true" if current filters match issues filtered search tokens', () => {
+ mockedState.filters = '?state=opened&label_name[]=Doing';
+ expect(hasFilters(mockedState)).toEqual(true);
+ });
+
+ it('should return "false" if current filters dont match issues filtered search tokens', () => {
+ mockedState.filters = '?scope=all&utf8=✓&state=opened';
+ expect(hasFilters(mockedState)).toEqual(false);
+ });
+
+ it('should return "false" if there are no filters', () => {
+ mockedState.filters = '';
+ expect(hasFilters(mockedState)).toEqual(false);
+ });
+ });
+});
diff --git a/spec/frontend/issues/stores/modules/issues_list/mutations_spec.js b/spec/frontend/issues/stores/modules/issues_list/mutations_spec.js
new file mode 100644
index 00000000000..15f9b07d1b1
--- /dev/null
+++ b/spec/frontend/issues/stores/modules/issues_list/mutations_spec.js
@@ -0,0 +1,66 @@
+import state from '~/issues/stores/modules/issues_list/state';
+import mutations from '~/issues/stores/modules/issues_list/mutations';
+import * as types from '~/issues/stores/modules/issues_list/mutation_types';
+
+describe('Issues List Mutataions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('SET_LOADING_STATE', () => {
+ it('sets the current loading state', () => {
+ mockedState.loading = false;
+ mutations[types.SET_LOADING_STATE](mockedState, true);
+
+ expect(mockedState.loading).toEqual(true);
+ });
+ });
+
+ describe('SET_FILTERS', () => {
+ it('sets the current filters', () => {
+ const mokedFilter = '?hello=worls';
+ mockedState.filters = 'none';
+ mutations[types.SET_FILTERS](mockedState, mokedFilter);
+
+ expect(mockedState.filters).toEqual(mokedFilter);
+ });
+ });
+
+ describe('SET_BULK_UPDATE_STATE', () => {
+ it('updates bulk update status', () => {
+ mockedState.isBulkUpdating = false;
+ mutations[types.SET_BULK_UPDATE_STATE](mockedState, true);
+
+ expect(mockedState.isBulkUpdating).toEqual(true);
+ });
+ });
+
+ describe('SET_TOTAL_ITEMS', () => {
+ it('sets the count of issues for pagination', () => {
+ mockedState.totalItems = 0;
+ mutations[types.SET_TOTAL_ITEMS](mockedState, '10');
+
+ expect(mockedState.totalItems).toEqual(10);
+ });
+ });
+
+ describe('SET_CURRENT_PAGE', () => {
+ it('sets the current page value', () => {
+ mockedState.currentPage = 0;
+ mutations[types.SET_CURRENT_PAGE](mockedState, '1');
+
+ expect(mockedState.currentPage).toEqual(1);
+ });
+ });
+
+ describe('SET_ISSUES_DATA', () => {
+ it('sets the current page value', () => {
+ mockedState.issues = null;
+ mutations[types.SET_ISSUES_DATA](mockedState, []);
+
+ expect(mockedState.issues).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index a986bc49f28..dbab2822a0e 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,11 +1,5 @@
import * as urlUtils from '~/lib/utils/url_utility';
-
-const setWindowLocation = value => {
- Object.defineProperty(window, 'location', {
- writable: true,
- value,
- });
-};
+import { setWindowLocation } from '../../helpers/url_util_helper';
describe('URL utility', () => {
describe('webIDEUrl', () => {