Commit b9592ccd authored by Coung Ngo's avatar Coung Ngo Committed by Jose Ivan Vargas

Add empty states and search to the issues list page refactor

parent 63c1ed12
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { toNumber } from 'lodash';
import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
......@@ -14,7 +14,7 @@ import {
} from '~/issues_list/constants';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
......@@ -26,13 +26,38 @@ export default {
sortParams,
i18n: {
calendarLabel: __('Subscribe to calendar'),
jiraIntegrationMessage: s__(
'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
),
jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'),
jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'),
newIssueLabel: __('New issue'),
noClosedIssuesTitle: __('There are no closed issues'),
noOpenIssuesDescription: __('To keep this project going, create a new issue'),
noOpenIssuesTitle: __('There are no open issues'),
noIssuesSignedInDescription: __(
'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.',
),
noIssuesSignedInTitle: __(
'The Issue Tracker is the place to add things that need to be improved or solved in a project',
),
noIssuesSignedOutButtonText: __('Register / Sign In'),
noIssuesSignedOutDescription: __(
'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.',
),
noIssuesSignedOutTitle: __('There are no issues to show'),
noSearchResultsDescription: __('To widen your search, change or remove filters above'),
noSearchResultsTitle: __('Sorry, your filter produced no results'),
reorderError: __('An error occurred while reordering issues.'),
rssLabel: __('Subscribe to RSS feed'),
},
components: {
CsvImportExportButtons,
GlButton,
GlEmptyState,
GlIcon,
GlLink,
GlSprintf,
IssuableList,
IssueCardTimeInfo,
BlockingIssuesCount: () => import('ee_component/issues/components/blocking_issues_count.vue'),
......@@ -47,6 +72,9 @@ export default {
canBulkUpdate: {
default: false,
},
emptyStateSvgPath: {
default: '',
},
endpoint: {
default: '',
},
......@@ -56,9 +84,18 @@ export default {
fullPath: {
default: '',
},
hasIssues: {
default: false,
},
isSignedIn: {
default: false,
},
issuesPath: {
default: '',
},
jiraIntegrationPath: {
default: '',
},
newIssuePath: {
default: '',
},
......@@ -68,6 +105,9 @@ export default {
showNewIssueLink: {
default: false,
},
signInPath: {
default: '',
},
},
data() {
const orderBy = getParameterByName('order_by');
......@@ -76,9 +116,18 @@ export default {
(key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort,
);
const search = getParameterByName('search') || '';
const tokens = search.split(' ').map((searchWord) => ({
type: 'filtered-search-term',
value: {
data: searchWord,
},
}));
return {
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filters: sortParams[sortKey] || {},
filterTokens: tokens,
isLoading: false,
issues: [],
page: toNumber(getParameterByName('page')) || 1,
......@@ -89,6 +138,23 @@ export default {
};
},
computed: {
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_ASC;
},
isOpenTab() {
return this.state === IssuableStates.Opened;
},
searchQuery() {
return (
this.filterTokens
.map((searchTerm) => searchTerm.value.data)
.filter((searchWord) => Boolean(searchWord))
.join(' ') || undefined
);
},
showPaginationControls() {
return this.issues.length > 0;
},
tabCounts() {
return Object.values(IssuableStates).reduce(
(acc, state) => ({
......@@ -101,13 +167,11 @@ export default {
urlParams() {
return {
page: this.page,
search: this.searchQuery,
state: this.state,
...this.filters,
};
},
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_ASC;
},
},
mounted() {
eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
......@@ -121,6 +185,10 @@ export default {
},
methods: {
fetchIssues() {
if (!this.hasIssues) {
return undefined;
}
this.isLoading = true;
return axios
......@@ -128,6 +196,7 @@ export default {
params: {
page: this.page,
per_page: this.$options.PAGE_SIZE,
search: this.searchQuery,
state: this.state,
with_labels_details: true,
...this.filters,
......@@ -166,6 +235,10 @@ export default {
this.state = state;
this.fetchIssues();
},
handleFilter(filter) {
this.filterTokens = filter;
this.fetchIssues();
},
handlePageChange(page) {
this.page = page;
this.fetchIssues();
......@@ -214,10 +287,12 @@ export default {
<template>
<issuable-list
v-if="hasIssues"
:namespace="fullPath"
recent-searches-storage-key="issues"
:search-input-placeholder="__('Search or filter results…')"
:search-tokens="[]"
:initial-filter-value="filterTokens"
:sort-options="$options.sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
......@@ -227,13 +302,14 @@ export default {
:issuables-loading="isLoading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="true"
:show-pagination-controls="showPaginationControls"
:total-items="totalIssues"
:current-page="page"
:previous-page="page - 1"
:next-page="page + 1"
:url-params="urlParams"
@click-tab="handleClickTab"
@filter="handleFilter"
@page-change="handlePageChange"
@reorder="handleReorder"
@sort="handleSort"
......@@ -267,7 +343,7 @@ export default {
{{ __('Edit issues') }}
</gl-button>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ __('New issue') }}
{{ $options.i18n.newIssueLabel }}
</gl-button>
</template>
......@@ -312,5 +388,81 @@ export default {
:is-list-item="true"
/>
</template>
<template #empty-state>
<gl-empty-state
v-if="searchQuery"
:description="$options.i18n.noSearchResultsDescription"
:title="$options.i18n.noSearchResultsTitle"
:svg-path="emptyStateSvgPath"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ $options.i18n.newIssueLabel }}
</gl-button>
</template>
</gl-empty-state>
<gl-empty-state
v-else-if="isOpenTab"
:description="$options.i18n.noOpenIssuesDescription"
:title="$options.i18n.noOpenIssuesTitle"
:svg-path="emptyStateSvgPath"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ $options.i18n.newIssueLabel }}
</gl-button>
</template>
</gl-empty-state>
<gl-empty-state
v-else
:title="$options.i18n.noClosedIssuesTitle"
:svg-path="emptyStateSvgPath"
/>
</template>
</issuable-list>
<div v-else-if="isSignedIn">
<gl-empty-state
:description="$options.i18n.noIssuesSignedInDescription"
:title="$options.i18n.noIssuesSignedInTitle"
:svg-path="emptyStateSvgPath"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ $options.i18n.newIssueLabel }}
</gl-button>
<csv-import-export-buttons
class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="totalIssues"
/>
</template>
</gl-empty-state>
<hr />
<p class="gl-text-center gl-font-weight-bold gl-mb-0">
{{ $options.i18n.jiraIntegrationTitle }}
</p>
<p class="gl-text-center gl-mb-0">
<gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
<template #jiraDocsLink="{ content }">
<gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p class="gl-text-center gl-text-gray-500">
{{ $options.i18n.jiraIntegrationSecondaryMessage }}
</p>
</div>
<gl-empty-state
v-else
:description="$options.i18n.noIssuesSignedOutDescription"
:title="$options.i18n.noIssuesSignedOutTitle"
:svg-path="emptyStateSvgPath"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
/>
</template>
......@@ -76,20 +76,26 @@ export function initIssuesListApp() {
calendarPath,
canBulkUpdate,
canEdit,
canImportIssues,
email,
emptyStateSvgPath,
endpoint,
exportCsvPath,
fullPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssues,
hasIssueWeightsFeature,
importCsvIssuesPath,
isSignedIn,
issuesPath,
jiraIntegrationPath,
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
rssPath,
showNewIssueLink,
signInPath,
} = el.dataset;
return new Vue({
......@@ -100,15 +106,20 @@ export function initIssuesListApp() {
provide: {
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
endpoint,
fullPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssues: parseBoolean(hasIssues),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
isSignedIn: parseBoolean(isSignedIn),
issuesPath,
jiraIntegrationPath,
newIssuePath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
// For CsvImportExportButtons component
canEdit: parseBoolean(canEdit),
email,
......@@ -116,8 +127,9 @@ export function initIssuesListApp() {
importCsvIssuesPath,
maxAttachmentSize,
projectImportJiraPath,
showExportButton: true,
showImportButton: true,
showExportButton: parseBoolean(hasIssues),
showImportButton: parseBoolean(canImportIssues),
showLabel: !parseBoolean(hasIssues),
},
render: (createComponent) => createComponent(IssuesListApp),
});
......
......@@ -168,17 +168,23 @@ module IssuesHelper
calendar_path: url_for(safe_params.merge(calendar_url_options)),
can_bulk_update: can?(current_user, :admin_issue, project).to_s,
can_edit: can?(current_user, :admin_project, project).to_s,
can_import_issues: can?(current_user, :import_issues, @project).to_s,
email: current_user&.notification_email,
empty_state_svg_path: image_path('illustrations/issues.svg'),
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
full_path: project.full_path,
has_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: import_csv_namespace_project_issues_path,
is_signed_in: current_user.present?.to_s,
issues_path: project_issues_path(project),
jira_integration_path: help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }),
project_import_jira_path: project_import_jira_path(project),
rss_path: url_for(safe_params.merge(rss_url_options)),
show_new_issue_link: show_new_issue_link?(project).to_s
show_new_issue_link: show_new_issue_link?(project).to_s,
sign_in_path: new_user_session_path
}
end
......
......@@ -17666,6 +17666,9 @@ msgstr ""
msgid "JiraService| on branch %{branch_link}"
msgstr ""
msgid "JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab."
msgstr ""
msgid "JiraService|%{jira_docs_link_start}Enable the Jira integration%{jira_docs_link_end} to view your Jira issues in GitLab."
msgstr ""
......
......@@ -288,24 +288,31 @@ RSpec.describe IssuesHelper do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:finder).and_return(finder)
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:url_for).and_return('#')
allow(helper).to receive(:image_path).and_return('#')
allow(helper).to receive(:import_csv_namespace_project_issues_path).and_return('#')
allow(helper).to receive(:url_for).and_return('#')
expected = {
calendar_path: '#',
can_bulk_update: 'true',
can_edit: 'true',
can_import_issues: 'true',
email: current_user&.notification_email,
empty_state_svg_path: '#',
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
full_path: project.full_path,
has_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#',
is_signed_in: current_user.present?.to_s,
issues_path: project_issues_path(project),
jira_integration_path: help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.id, milestone_id: finder.milestones.first.id }),
project_import_jira_path: project_import_jira_path(project),
rss_path: '#',
show_new_issue_link: 'true'
show_new_issue_link: 'true',
sign_in_path: new_user_session_path
}
expect(helper.issues_list_data(project, current_user, finder)).to include(expected)
......
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