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> <script>
import { toNumber, omit } from 'lodash'; 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 flash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
...@@ -23,9 +28,13 @@ import { ...@@ -23,9 +28,13 @@ import {
} from '../constants'; } from '../constants';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
import issueableEventHub from '../eventhub'; import issueableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
export default { export default {
LOADING_LIST_ITEMS_LENGTH, LOADING_LIST_ITEMS_LENGTH,
directives: {
SafeHtml,
},
components: { components: {
GlEmptyState, GlEmptyState,
GlPagination, GlPagination,
...@@ -39,15 +48,9 @@ export default { ...@@ -39,15 +48,9 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
createIssuePath: { emptyStateMeta: {
type: String, type: Object,
required: false, required: true,
default: '',
},
emptySvgPath: {
type: String,
required: false,
default: '',
}, },
endpoint: { endpoint: {
type: String, type: String,
...@@ -94,28 +97,40 @@ export default { ...@@ -94,28 +97,40 @@ export default {
emptyState() { emptyState() {
if (this.issuables.length) { if (this.issuables.length) {
return {}; // Empty state shouldn't be shown here return {}; // Empty state shouldn't be shown here
} else if (this.hasFilters) { }
if (this.isServiceDesk) {
return emptyStateHelper(this.emptyStateMeta);
}
if (this.hasFilters) {
return { return {
title: __('Sorry, your filter produced no results'), title: __('Sorry, your filter produced no results'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To widen your search, change or remove filters above'), description: __('To widen your search, change or remove filters above'),
primaryLink: this.createIssuePath, primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'), primaryText: __('New issue'),
}; };
} else if (this.filters.state === 'opened') { }
if (this.filters.state === 'opened') {
return { return {
title: __('There are no open issues'), title: __('There are no open issues'),
svgPath: this.emptyStateMeta.svgPath,
description: __('To keep this project going, create a new issue'), description: __('To keep this project going, create a new issue'),
primaryLink: this.createIssuePath, primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'), primaryText: __('New issue'),
}; };
} else if (this.filters.state === 'closed') { } else if (this.filters.state === 'closed') {
return { return {
title: __('There are no closed issues'), title: __('There are no closed issues'),
svgPath: this.emptyStateMeta.svgPath,
}; };
} }
return { return {
title: __('There are no issues to show'), title: __('There are no issues to show'),
svgPath: this.emptyStateMeta.svgPath,
description: __( 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.', '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 { ...@@ -157,6 +172,9 @@ export default {
nextPage: this.paginationNext, nextPage: this.paginationNext,
}; };
}, },
isServiceDesk() {
return this.type === 'service_desk';
},
isJira() { isJira() {
return this.type === 'jira'; return this.type === 'jira';
}, },
...@@ -394,10 +412,13 @@ export default { ...@@ -394,10 +412,13 @@ export default {
<gl-empty-state <gl-empty-state
v-else v-else
:title="emptyState.title" :title="emptyState.title"
:description="emptyState.description" :svg-path="emptyState.svgPath"
:svg-path="emptySvgPath"
:primary-button-link="emptyState.primaryLink" :primary-button-link="emptyState.primaryLink"
:primary-button-text="emptyState.primaryText" :primary-button-text="emptyState.primaryText"
/> >
<template #description>
<div v-safe-html="emptyState.description"></div>
</template>
</gl-empty-state>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; 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 IssuableListRootApp from './components/issuable_list_root_app.vue';
import IssuablesListApp from './components/issuables_list_app.vue'; import IssuablesListApp from './components/issuables_list_app.vue';
...@@ -41,7 +41,7 @@ function mountIssuablesListApp() { ...@@ -41,7 +41,7 @@ function mountIssuablesListApp() {
} }
document.querySelectorAll('.js-issuables-list').forEach(el => { document.querySelectorAll('.js-issuables-list').forEach(el => {
const { canBulkEdit, ...data } = el.dataset; const { canBulkEdit, emptyStateMeta = {}, ...data } = el.dataset;
return new Vue({ return new Vue({
el, el,
...@@ -49,6 +49,10 @@ function mountIssuablesListApp() { ...@@ -49,6 +49,10 @@ function mountIssuablesListApp() {
return createElement(IssuablesListApp, { return createElement(IssuablesListApp, {
props: { props: {
...data, ...data,
emptyStateMeta:
Object.keys(emptyStateMeta).length !== 0
? convertObjectPropsToCamelCase(JSON.parse(emptyStateMeta))
: {},
canBulkEdit: Boolean(canBulkEdit), 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 FilteredSearchServiceDesk from './filtered_search';
import initIssuablesList from '~/issuables_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse( const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot, 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 @@ ...@@ -25,7 +25,7 @@
- if Feature.enabled?(:vue_issuables_list, @group) - if Feature.enabled?(:vue_issuables_list, @group)
.js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)), .js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)),
'can-bulk-edit': @can_bulk_update.to_json, '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 } } 'sort-key': @sort } }
- else - else
= render 'shared/issues' = render 'shared/issues'
- if Feature.enabled?(:vue_issuables_list, @project) - if Feature.enabled?(:vue_issuables_list, @project)
.js-issuables-list{ data: { endpoint: expose_url(api_v4_projects_issues_path(id: @project.id)), - data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
'create_issue_path': expose_url(new_project_issue_path(@project)), - 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, '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 - else
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues') - 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') } %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? - 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) - 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") - 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? .non-empty-state.media
%div{ class: "#{callout_selector}" } .svg-content
.svg-content = render 'shared/empty_states/icons/service_desk_callout.svg'
= render svg_path
%div{ class: is_empty_state ? "text-content" : "gl-mt-3 gl-ml-3" } .gl-mt-3.gl-ml-3
- if is_empty_state %h5= title_text
%h4= title_text
- else
%h5= title_text
- if can_edit_project_settings && service_desk_enabled - if can_edit_project_settings && service_desk_enabled
%p %p
= _("Have your users email") = _("Have your users email")
%code= @project.service_desk_address %code= @project.service_desk_address
%span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.") %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') = link_to _('Read more'), help_page_path('user/project/service_desk')
- if can_edit_project_settings && !service_desk_enabled - if can_edit_project_settings && !service_desk_enabled
%div{ class: is_empty_state ? "text-center" : "gl-mt-3" } .gl-mt-3
= link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success' = 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')
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false = 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 .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls.d-block.d-sm-none .nav-controls.d-block.d-sm-none
...@@ -15,7 +17,15 @@ ...@@ -15,7 +17,15 @@
- if @issues.present? - if @issues.present?
= render 'shared/issuable/search_bar', type: :issues = 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 .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 @@ ...@@ -4,9 +4,9 @@
= render 'shared/issuable/nav', type: :issues, display_count: false = render 'shared/issuable/nav', type: :issues, display_count: false
= render 'projects/integrations/jira/issues/nav_btns' = 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, '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, 'sort-key': @sort,
type: 'jira', type: 'jira',
project_path: @project.full_path, } } project_path: @project.full_path, } }
...@@ -7,8 +7,6 @@ RSpec.describe 'Service Desk Issue Tracker', :js do ...@@ -7,8 +7,6 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
stub_feature_flags(vue_issuables_list: false)
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).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 ...@@ -78,11 +76,9 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
context 'when service desk has been activated' do context 'when service desk has been activated' do
context 'when there are no issues' do context 'when there are no issues' do
describe 'service desk info content' 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) visit service_desk_project_issues_path(project)
end
it 'displays the large info box, documentation, and the address' do
aggregate_failures do aggregate_failures do
expect(page).to have_css('.empty-state') expect(page).to have_css('.empty-state')
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk')) expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
......
...@@ -2,7 +2,6 @@ ...@@ -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`] = ` 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 <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" svgpath="/emptySvg"
title="There are no issues to show" title="There are no issues to show"
/> />
......
...@@ -21,7 +21,7 @@ jest.mock('~/lib/utils/common_utils', () => ({ ...@@ -21,7 +21,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
const TEST_LOCATION = `${TEST_HOST}/issues`; const TEST_LOCATION = `${TEST_HOST}/issues`;
const TEST_ENDPOINT = '/issues'; const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue'; const TEST_CREATE_ISSUES_PATH = '/createIssue';
const TEST_EMPTY_SVG_PATH = '/emptySvg'; const TEST_SVG_PATH = '/emptySvg';
const setUrl = query => { const setUrl = query => {
window.location.href = `${TEST_LOCATION}${query}`; window.location.href = `${TEST_LOCATION}${query}`;
...@@ -48,11 +48,15 @@ describe('Issuables list component', () => { ...@@ -48,11 +48,15 @@ describe('Issuables list component', () => {
}; };
const factory = (props = { sortKey: 'priority' }) => { const factory = (props = { sortKey: 'priority' }) => {
const emptyStateMeta = {
createIssuePath: TEST_CREATE_ISSUES_PATH,
svgPath: TEST_SVG_PATH,
};
wrapper = shallowMount(IssuablesListApp, { wrapper = shallowMount(IssuablesListApp, {
propsData: { propsData: {
endpoint: TEST_ENDPOINT, endpoint: TEST_ENDPOINT,
createIssuePath: TEST_CREATE_ISSUES_PATH, emptyStateMeta,
emptySvgPath: TEST_EMPTY_SVG_PATH,
...props, ...props,
}, },
}); });
...@@ -117,9 +121,10 @@ describe('Issuables list component', () => { ...@@ -117,9 +121,10 @@ describe('Issuables list component', () => {
expect(wrapper.vm).toMatchObject({ expect(wrapper.vm).toMatchObject({
// Props // Props
canBulkEdit: false, canBulkEdit: false,
createIssuePath: TEST_CREATE_ISSUES_PATH, emptyStateMeta: {
emptySvgPath: TEST_EMPTY_SVG_PATH, createIssuePath: TEST_CREATE_ISSUES_PATH,
svgPath: TEST_SVG_PATH,
},
// Data // Data
filters: { filters: {
state: 'opened', 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