Commit dbc0b89d authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '233725-add-vue-issuables-list' into 'master'

Refactor the service desk list to use Vue

See merge request gitlab-org/gitlab!39169
parents 504ee264 34f779c2
<script>
import { toNumber, omit } from 'lodash';
import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
import {
GlEmptyState,
GlPagination,
GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
......@@ -23,9 +28,13 @@ import {
} from '../constants';
import { setUrlParams } from '~/lib/utils/url_utility';
import issueableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
export default {
LOADING_LIST_ITEMS_LENGTH,
directives: {
SafeHtml,
},
components: {
GlEmptyState,
GlPagination,
......@@ -39,15 +48,9 @@ export default {
required: false,
default: false,
},
createIssuePath: {
type: String,
required: false,
default: '',
},
emptySvgPath: {
type: String,
required: false,
default: '',
emptyStateMeta: {
type: Object,
required: true,
},
endpoint: {
type: String,
......@@ -94,28 +97,40 @@ export default {
emptyState() {
if (this.issuables.length) {
return {}; // Empty state shouldn't be shown here
} else if (this.hasFilters) {
}
if (this.isServiceDesk) {
return emptyStateHelper(this.emptyStateMeta);
}
if (this.hasFilters) {
return {
title: __('Sorry, your filter produced no results'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To widen your search, change or remove filters above'),
primaryLink: this.createIssuePath,
primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'),
};
} else if (this.filters.state === 'opened') {
}
if (this.filters.state === 'opened') {
return {
title: __('There are no open issues'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To keep this project going, create a new issue'),
primaryLink: this.createIssuePath,
primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'),
};
} else if (this.filters.state === 'closed') {
return {
title: __('There are no closed issues'),
svgPath: this.emptyStateMeta.svgPath,
};
}
return {
title: __('There are no issues to show'),
svgPath: this.emptyStateMeta.svgPath,
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.',
),
......@@ -157,6 +172,9 @@ export default {
nextPage: this.paginationNext,
};
},
isServiceDesk() {
return this.type === 'service_desk';
},
isJira() {
return this.type === 'jira';
},
......@@ -394,10 +412,13 @@ export default {
<gl-empty-state
v-else
:title="emptyState.title"
:description="emptyState.description"
:svg-path="emptySvgPath"
:svg-path="emptyState.svgPath"
:primary-button-link="emptyState.primaryLink"
:primary-button-text="emptyState.primaryText"
/>
>
<template #description>
<div v-safe-html="emptyState.description"></div>
</template>
</gl-empty-state>
</div>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssuableListRootApp from './components/issuable_list_root_app.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
......@@ -41,7 +41,7 @@ function mountIssuablesListApp() {
}
document.querySelectorAll('.js-issuables-list').forEach(el => {
const { canBulkEdit, ...data } = el.dataset;
const { canBulkEdit, emptyStateMeta = {}, ...data } = el.dataset;
return new Vue({
el,
......@@ -49,6 +49,10 @@ function mountIssuablesListApp() {
return createElement(IssuablesListApp, {
props: {
...data,
emptyStateMeta:
Object.keys(emptyStateMeta).length !== 0
? convertObjectPropsToCamelCase(JSON.parse(emptyStateMeta))
: {},
canBulkEdit: Boolean(canBulkEdit),
},
});
......
import { __ } from '~/locale';
/**
* Returns the attributes used for gl-empty-state in the Service Desk issues list.
*/
// eslint-disable-next-line import/prefer-default-export
export function emptyStateHelper(emptyStateMeta) {
const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta;
if (isServiceDeskSupported) {
const title = __(
'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
);
const commonMessage = __(
'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
);
const commonDescription = `
<span>${commonMessage}</span>
<a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
return {
title,
svgPath,
description: `<p>${__('Have your users email')} <code>${
emptyStateMeta.serviceDeskAddress
}</code></p> ${commonDescription}`,
};
}
if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) {
return {
title,
svgPath,
description: commonDescription,
primaryLink: emptyStateMeta.editProjectPage,
primaryText: __('Turn on Service Desk'),
};
}
return {
title,
svgPath,
description: commonDescription,
};
}
return {
title: __('Service Desk is enabled but not yet active'),
svgPath,
description: __('You must set up incoming email before it becomes active.'),
primaryLink: emptyStateMeta.incomingEmailHelpPage,
primaryText: __('More information'),
};
}
import FilteredSearchServiceDesk from './filtered_search';
import initIssuablesList from '~/issuables_list';
document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot,
);
const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
if (document.querySelector('.filtered-search')) {
const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
filteredSearchManager.setup();
}
filteredSearchManager.setup();
if (gon.features?.vueIssuablesList) {
initIssuablesList();
}
});
# frozen_string_literal: true
module Projects::Issues::ServiceDeskHelper
def service_desk_meta(project)
empty_state_meta = {
is_service_desk_supported: Gitlab::ServiceDesk.supported?,
is_service_desk_enabled: project.service_desk_enabled?,
can_edit_project_settings: can?(current_user, :admin_project, project)
}
if Gitlab::ServiceDesk.supported?
empty_state_meta.merge(supported_meta(project))
else
empty_state_meta.merge(unsupported_meta(project))
end
end
private
def supported_meta(project)
{
service_desk_address: project.service_desk_address,
service_desk_help_page: help_page_path('user/project/service_desk'),
edit_project_page: edit_project_path(project),
svg_path: image_path('illustrations/service_desk_empty.svg')
}
end
def unsupported_meta(project)
{
incoming_email_help_page: help_page_path('administration/incoming_email', anchor: 'set-it-up'),
svg_path: image_path('illustrations/service-desk-setup.svg')
}
end
end
......@@ -25,7 +25,7 @@
- if Feature.enabled?(:vue_issuables_list, @group)
.js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)),
'can-bulk-edit': @can_bulk_update.to_json,
'empty-svg-path': image_path('illustrations/issues.svg'),
'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') },
'sort-key': @sort } }
- else
= render 'shared/issues'
- if Feature.enabled?(:vue_issuables_list, @project)
.js-issuables-list{ data: { endpoint: expose_url(api_v4_projects_issues_path(id: @project.id)),
'create_issue_path': expose_url(new_project_issue_path(@project)),
- data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
- default_empty_state_meta = { create_issue_path: new_project_issue_path(@project), svg_path: image_path('illustrations/issues.svg') }
- data_empty_state_meta = local_assigns.fetch(:data_empty_state_meta, default_empty_state_meta)
- type = local_assigns.fetch(:type, '')
.js-issuables-list{ data: { endpoint: data_endpoint,
'empty-state-meta': data_empty_state_meta.to_json,
'can-bulk-edit': @can_bulk_update.to_json,
'empty-svg-path': image_path('illustrations/issues.svg'),
'sort-key': @sort } }
'sort-key': @sort,
'type': type } }
- else
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
......
- service_desk_enabled = @project.service_desk_enabled?
- can_edit_project_settings = can?(current_user, :admin_project, @project)
- title_text = _("Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab")
- if Gitlab::ServiceDesk.supported?
.empty-state
.svg-content
= render 'shared/empty_states/icons/service_desk_empty_state.svg'
.text-content
%h4= title_text
- if can_edit_project_settings && service_desk_enabled
%p
= _("Have your users email")
%code= @project.service_desk_address
%span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
= link_to _('Read more'), help_page_path('user/project/service_desk')
- if can_edit_project_settings && !service_desk_enabled
.text-center
= link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
- else
.empty-state
.svg-content
= render 'shared/empty_states/icons/service_desk_setup.svg'
.text-content
%h4= _('Service Desk is enabled but not yet active')
%p
= _("You must set up incoming email before it becomes active.")
= link_to _('More information'), help_page_path('administration/incoming_email', anchor: 'set-it-up')
- is_empty_state = @issues.blank?
- service_desk_enabled = @project.service_desk_enabled?
- callout_selector = is_empty_state ? 'empty-state' : 'non-empty-state media'
- svg_path = !is_empty_state ? 'shared/empty_states/icons/service_desk_callout.svg' : 'shared/empty_states/icons/service_desk_empty_state.svg'
- can_edit_project_settings = can?(current_user, :admin_project, @project)
- title_text = _("Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab")
- if Gitlab::ServiceDesk.supported?
%div{ class: "#{callout_selector}" }
.svg-content
= render svg_path
.non-empty-state.media
.svg-content
= render 'shared/empty_states/icons/service_desk_callout.svg'
%div{ class: is_empty_state ? "text-content" : "gl-mt-3 gl-ml-3" }
- if is_empty_state
%h4= title_text
- else
%h5= title_text
.gl-mt-3.gl-ml-3
%h5= title_text
- if can_edit_project_settings && service_desk_enabled
%p
= _("Have your users email")
%code= @project.service_desk_address
- if can_edit_project_settings && service_desk_enabled
%p
= _("Have your users email")
%code= @project.service_desk_address
%span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
= link_to _('Read more'), help_page_path('user/project/service_desk')
%span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
= link_to _('Read more'), help_page_path('user/project/service_desk')
- if can_edit_project_settings && !service_desk_enabled
%div{ class: is_empty_state ? "text-center" : "gl-mt-3" }
= link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
- else
.empty-state
.svg-content
= render 'shared/empty_states/icons/service_desk_setup.svg'
.text-content
%h4= _('Service Desk is enabled but not yet active')
%p
= _("You must set up incoming email before it becomes active.")
= link_to _('More information'), help_page_path('administration/incoming_email', anchor: 'set-it-up')
- if can_edit_project_settings && !service_desk_enabled
.gl-mt-3
= link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
......@@ -5,9 +5,11 @@
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
- support_bot_attrs = UserSerializer.new.represent(User.support_bot).to_json
- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(User.support_bot) }.to_json
%div{ class: "js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs } }
- data_endpoint = "#{expose_path(api_v4_projects_issues_path(id: @project.id))}?author_id=#{User.support_bot.id}"
%div{ class: "js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs, service_desk_meta: service_desk_meta(@project) } }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls.d-block.d-sm-none
......@@ -15,7 +17,15 @@
- if @issues.present?
= render 'shared/issuable/search_bar', type: :issues
= render 'service_desk_info_content'
- if Gitlab::ServiceDesk.supported?
= render 'service_desk_info_content'
-# TODO Remove empty_state_path once vue_issuables_list FF is removed.
-# https://gitlab.com/gitlab-org/gitlab/-/issues/235652
-# `empty_state_path` is used to render the empty state in the HAML version of issuables list.
.issues-holder
= render 'projects/issues/issues', empty_state_path: 'service_desk_info_content'
= render 'projects/issues/issues',
empty_state_path: 'service_desk_empty_state',
data_endpoint: data_endpoint,
data_empty_state_meta: service_desk_meta(@project),
type: 'service_desk'
......@@ -4,9 +4,9 @@
= render 'shared/issuable/nav', type: :issues, display_count: false
= render 'projects/integrations/jira/issues/nav_btns'
.js-issuables-list{ data: { endpoint: expose_path(project_integrations_jira_issues_path(@project, format: :json)),
.js-issuables-list{ data: { endpoint: project_integrations_jira_issues_path(@project, format: :json),
'can-bulk-edit': false,
'empty-svg-path': image_path('illustrations/issues.svg'),
'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') },
'sort-key': @sort,
type: 'jira',
project_path: @project.full_path, } }
......@@ -7,8 +7,6 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(vue_issuables_list: false)
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
......@@ -78,11 +76,9 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
context 'when service desk has been activated' do
context 'when there are no issues' do
describe 'service desk info content' do
before do
it 'displays the large info box, documentation, and the address' do
visit service_desk_project_issues_path(project)
end
it 'displays the large info box, documentation, and the address' do
aggregate_failures do
expect(page).to have_css('.empty-state')
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
......
......@@ -2,7 +2,6 @@
exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = `
<gl-empty-state-stub
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."
svgpath="/emptySvg"
title="There are no issues to show"
/>
......
......@@ -21,7 +21,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
const TEST_LOCATION = `${TEST_HOST}/issues`;
const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue';
const TEST_EMPTY_SVG_PATH = '/emptySvg';
const TEST_SVG_PATH = '/emptySvg';
const setUrl = query => {
window.location.href = `${TEST_LOCATION}${query}`;
......@@ -48,11 +48,15 @@ describe('Issuables list component', () => {
};
const factory = (props = { sortKey: 'priority' }) => {
const emptyStateMeta = {
createIssuePath: TEST_CREATE_ISSUES_PATH,
svgPath: TEST_SVG_PATH,
};
wrapper = shallowMount(IssuablesListApp, {
propsData: {
endpoint: TEST_ENDPOINT,
createIssuePath: TEST_CREATE_ISSUES_PATH,
emptySvgPath: TEST_EMPTY_SVG_PATH,
emptyStateMeta,
...props,
},
});
......@@ -117,9 +121,10 @@ describe('Issuables list component', () => {
expect(wrapper.vm).toMatchObject({
// Props
canBulkEdit: false,
createIssuePath: TEST_CREATE_ISSUES_PATH,
emptySvgPath: TEST_EMPTY_SVG_PATH,
emptyStateMeta: {
createIssuePath: TEST_CREATE_ISSUES_PATH,
svgPath: TEST_SVG_PATH,
},
// Data
filters: {
state: 'opened',
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Issues::ServiceDeskHelper do
let_it_be(:project) { create(:project, :public, service_desk_enabled: true) }
let(:user) { build_stubbed(:user) }
let(:current_user) { user }
describe '#service_desk_meta' do
subject { helper.service_desk_meta(project) }
context "when service desk is supported and user can edit project settings" do
before do
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(current_user, :admin_project, project).and_return(true)
end
it {
is_expected.to eq({
is_service_desk_supported: true,
is_service_desk_enabled: true,
can_edit_project_settings: true,
service_desk_address: project.service_desk_address,
service_desk_help_page: help_page_path('user/project/service_desk'),
edit_project_page: edit_project_path(project),
svg_path: ActionController::Base.helpers.image_path('illustrations/service_desk_empty.svg')
})
}
end
context "when service desk is not supported and user cannot edit project settings" do
before do
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(false)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(false)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(current_user, :admin_project, project).and_return(false)
end
it {
is_expected.to eq({
is_service_desk_supported: false,
is_service_desk_enabled: false,
can_edit_project_settings: false,
incoming_email_help_page: help_page_path('administration/incoming_email', anchor: 'set-it-up'),
svg_path: ActionController::Base.helpers.image_path('illustrations/service-desk-setup.svg')
})
}
end
end
end
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