Commit f39f0af7 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch '3049-service-desk-should-be-given-more-prominence-in-the-ui' into 'master'

Give Service Desk more prominence in the UI

Closes #3049

See merge request !2733
parents 0e0dcf00 a8e858bb
...@@ -345,7 +345,14 @@ class FilteredSearchManager { ...@@ -345,7 +345,14 @@ class FilteredSearchManager {
const removeElements = []; const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => { [].forEach.call(this.tokensContainer.children, (t) => {
if (t.classList.contains('js-visual-token')) { let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim();
canClearToken = this.canEdit && this.canEdit(tokenKey);
}
if (canClearToken) {
removeElements.push(t); removeElements.push(t);
} }
}); });
...@@ -424,8 +431,14 @@ class FilteredSearchManager { ...@@ -424,8 +431,14 @@ class FilteredSearchManager {
}); });
} }
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
getAllParams(urlParams) {
return this.modifyUrlParams ? this.modifyUrlParams(urlParams) : urlParams;
}
loadSearchParamsFromURL() { loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray(); const urlParams = gl.utils.getUrlParamsArray();
const params = this.getAllParams(urlParams);
const usernameParams = this.getUsernameParams(); const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false; let hasFilteredSearch = false;
......
/* eslint-disable class-methods-use-this */
const AUTHOR_PARAM_KEY = 'author_username';
export default class FilteredSearchServiceDesk extends gl.FilteredSearchManager {
constructor(supportBotData) {
super('service_desk');
this.supportBotData = supportBotData;
}
canEdit(tokenName) {
return tokenName !== 'author';
}
modifyUrlParams(paramsArray) {
const supportBotParamPair = `${AUTHOR_PARAM_KEY}=${this.supportBotData.username}`;
const onlyValidParams = paramsArray.filter(param => param.indexOf(AUTHOR_PARAM_KEY) === -1);
// unshift ensures author param is always first token element
onlyValidParams.unshift(supportBotParamPair);
return onlyValidParams;
}
}
import FilteredSearchServiceDesk from './filtered_search';
document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot,
);
this.filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
this.filteredSearchManager.setup();
});
...@@ -43,3 +43,9 @@ ...@@ -43,3 +43,9 @@
* Styles for JS behaviors. * Styles for JS behaviors.
*/ */
@import "behaviors"; @import "behaviors";
/*
* EE-only stylesheets
*/
@import "../../../ee/app/assets/stylesheets/**/*";
...@@ -10,6 +10,23 @@ module IssuableCollections ...@@ -10,6 +10,23 @@ module IssuableCollections
private private
def set_issues_index
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
@users = []
end
def issues_collection def issues_collection
issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace) issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
end end
......
...@@ -6,12 +6,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -6,12 +6,11 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include SpammableActions include SpammableActions
prepend ::EE::Projects::IssuesController
prepend_before_action :authenticate_user!, only: [:new, :export_csv] prepend_before_action :authenticate_user!, only: [:new, :export_csv]
before_action :check_issues_available! before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv] before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
before_action :set_issues_index, only: [:index]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -22,25 +21,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -22,25 +21,11 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
prepend ::EE::Projects::IssuesController
respond_to :html respond_to :html
def index def index
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
@users = []
if params[:assignee_id].present? if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id]) assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee @users.push(assignee) if assignee
......
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
= number_with_delimiter(@project.open_issues_count) = number_with_delimiter(@project.open_issues_count)
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :issues, action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do = link_to project_issues_path(@project) do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Issues') } #{ _('Issues') }
...@@ -147,6 +147,11 @@ ...@@ -147,6 +147,11 @@
%span %span
Labels Labels
- if EE::Gitlab::ServiceDesk.enabled?(project: @project)
= nav_link(controller: :issues, action: :service_desk ) do
= link_to service_desk_project_issues_path(@project), title: 'Service Desk' do
%span Service Desk
= nav_link(controller: :milestones) do = nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do = link_to project_milestones_path(@project), title: 'Milestones' do
%span %span
......
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
%ul.content-list.issues-list.issuable-list %ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues = render partial: "projects/issues/issue", collection: @issues
- if @issues.blank? - if @issues.blank?
= render 'shared/empty_states/issues' = render empty_state_path
- if @issues.present? - if @issues.present?
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do - show_rss_button = local_assigns.fetch(:show_rss_button, true)
= icon('rss') - show_export_button = local_assigns.fetch(:show_export_button, true)
= render 'projects/issues/export_issues/button'
- if show_rss_button
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if show_export_button
= render 'projects/issues/export_issues/button'
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project, = link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id), issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }), milestone_id: issues_finder.milestones.first.try(:id) }),
......
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><rect width="7" height="1" x="59" y="38" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M60.5 42a3.5 3.5 0 0 0 0-7v7z"/><rect width="7" height="1" x="12" y="38" fill="#E1DBF2" transform="matrix(-1 0 0 1 31 0)" rx=".5"/><path fill="#6B4FBB" d="M17.5 42a3.5 3.5 0 0 1 0-7v7z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M39 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M35 56a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M26.5 40c0 4.143 3.355 7.5 7.494 7.5h10.012A7.497 7.497 0 0 0 51.5 40c0-4.143-3.355-7.5-7.494-7.5H33.994A7.497 7.497 0 0 0 26.5 40zm-3 0c0-5.799 4.698-10.5 10.494-10.5h10.012C49.802 29.5 54.5 34.2 54.5 40c0 5.799-4.698 10.5-10.494 10.5H33.994C28.198 50.5 23.5 45.8 23.5 40z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M35.255 42.406a1 1 0 1 1 1.872-.703 2.001 2.001 0 0 0 3.76-.038 1 1 0 1 1 1.886.665 4 4 0 0 1-7.518.076zM31.5 40a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm15 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#6B4FBB" d="M38 22h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zm0 3h2a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2z" style="mix-blend-mode:multiply"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="226" height="178" viewBox="0 0 226 178"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M109.496 165.895c2.06.108 4.113.134 6.158.08 1.104-.03 1.975-.95 1.945-2.055-.03-1.104-.95-1.975-2.055-1.945-1.94.053-3.886.028-5.84-.074-1.102-.057-2.043.79-2.1 1.893-.06 1.104.788 2.045 1.89 2.102zm18.408-1.245c2.02-.386 4.023-.853 6-1.4 1.066-.295 1.69-1.396 1.396-2.46-.295-1.066-1.397-1.69-2.46-1.396-1.875.52-3.772.96-5.686 1.327-1.085.208-1.797 1.255-1.59 2.34.207 1.085 1.255 1.797 2.34 1.59zm17.572-5.636c1.865-.86 3.696-1.795 5.486-2.803.962-.54 1.303-1.76.762-2.723-.542-.962-1.762-1.303-2.724-.762-1.697.955-3.43 1.84-5.2 2.656-1.002.464-1.44 1.652-.978 2.655.462 1.003 1.65 1.44 2.654.98zm44.342-74.897c-.142-2.056-.367-4.1-.674-6.127-.165-1.092-1.184-1.844-2.276-1.678-1.092.165-1.844 1.184-1.68 2.276.29 1.92.505 3.857.64 5.805.076 1.102 1.03 1.934 2.133 1.857 1.103-.076 1.934-1.03 1.858-2.133zm-3.505-18.144c-.632-1.956-1.343-3.884-2.13-5.78-.425-1.02-1.595-1.504-2.615-1.08-1.02.424-1.503 1.594-1.08 2.614.747 1.797 1.42 3.624 2.02 5.476.34 1.05 1.467 1.628 2.518 1.288 1.05-.34 1.627-1.466 1.287-2.517zm-7.754-16.73c-1.083-1.745-2.235-3.447-3.454-5.1-.655-.89-1.907-1.08-2.797-.423-.89.655-1.08 1.907-.424 2.796 1.155 1.568 2.247 3.18 3.273 4.835.58.94 1.814 1.23 2.753.647.938-.582 1.228-1.815.646-2.754zm-11.582-14.446c-1.468-1.437-2.993-2.814-4.572-4.128-.85-.708-2.11-.592-2.816.256-.707.85-.592 2.11.257 2.817 1.496 1.246 2.942 2.55 4.334 3.913.79.773 2.057.76 2.83-.03.772-.79.758-2.057-.032-2.83zm-101.422-4.91c-1.6 1.288-3.148 2.64-4.64 4.05-.802.76-.837 2.026-.078 2.828.76.802 2.025.837 2.827.078 1.415-1.338 2.882-2.62 4.4-3.84.86-.692.996-1.95.303-2.812-.692-.86-1.95-.996-2.812-.303zM52.7 43.062c-1.25 1.632-2.433 3.313-3.546 5.04-.6.93-.33 2.167.597 2.765.93.6 2.167.33 2.766-.597 1.055-1.637 2.176-3.23 3.36-4.777.67-.878.504-2.133-.374-2.804-.877-.672-2.132-.505-2.803.372zm-9.373 15.924c-.82 1.882-1.56 3.8-2.226 5.745-.356 1.047.2 2.183 1.247 2.54 1.045.358 2.182-.2 2.54-1.246.63-1.844 1.333-3.66 2.108-5.443.44-1.012-.023-2.19-1.036-2.63-1.014-.44-2.192.023-2.633 1.036zm-5.26 17.74c-.34 2.02-.6 4.058-.777 6.11-.096 1.102.72 2.07 1.82 2.167 1.1.095 2.07-.72 2.165-1.82.17-1.947.415-3.88.737-5.793.183-1.09-.552-2.12-1.64-2.304-1.09-.183-2.122.552-2.305 1.64zM74.87 155.55c1.772 1.038 3.585 2.005 5.437 2.897.995.48 2.19.062 2.67-.933.48-.995.062-2.19-.933-2.67-1.755-.845-3.473-1.76-5.152-2.745-.953-.56-2.178-.24-2.737.714-.558.954-.238 2.18.715 2.738zm16.97 7.34c1.966.578 3.96 1.078 5.975 1.498 1.082.225 2.14-.47 2.366-1.55.226-1.082-.468-2.14-1.55-2.366-1.91-.398-3.798-.872-5.662-1.42-1.06-.312-2.172.294-2.483 1.354-.312 1.06.294 2.17 1.354 2.483z"/><path fill="#F9F9F9" d="M2.12 130c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M39 166c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 92 39 92 4 107.67 4 127s15.67 35 35 35z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M53.925 116.226c-.277-.144-.59-.226-.925-.226H25c-.323 0-.628.076-.898.212l14.663 13.406c.39.357.99.348 1.37-.02l13.79-13.372zm1.075 4.53L42.92 132.47c-1.898 1.84-4.902 1.885-6.854.1L23 120.624V138c0 1.105.895 2 2 2h28c1.105 0 2-.895 2-2v-17.244zM25 112h28c3.314 0 6 2.686 6 6v20c0 3.314-2.686 6-6 6H25c-3.314 0-6-2.686-6-6v-20c0-3.314 2.686-6 6-6z"/><g><path fill="#F9F9F9" d="M150.12 131c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M187 167c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M180.51 137H199c1.105 0 2-.895 2-2v-16c0-1.105-.895-2-2-2h-24c-1.105 0-2 .895-2 2v22.743l7.51-4.743zm1.157 4l-9.6 6.062c-.32.202-.69.31-1.067.31-1.105 0-2-.896-2-2V119c0-3.314 2.686-6 6-6h24c3.314 0 6 2.686 6 6v16c0 3.314-2.686 6-6 6h-17.333z"/><path fill="#6B4FBB" d="M180 129c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm7 0c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm7 0c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/></g><g><path fill="#F9F9F9" d="M76.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3-1.527 19.032-17.455 34-36.88 34-19.425 0-35.353-14.968-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M113 78c-21.54 0-39-17.46-39-39S91.46 0 113 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S132.33 4 113 4 78 19.67 78 39s15.67 35 35 35z"/><g transform="translate(133 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7C3.433 7 5 5.433 5 3.5S3.433 0 1.5 0v7z"/></g><g transform="matrix(-1 0 0 1 93 35)"><rect width="7" height="1" y="3" fill="#E1DBF2" rx=".5"/><path fill="#6B4FBB" d="M1.5 7C3.433 7 5 5.433 5 3.5S3.433 0 1.5 0v7z"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M113 58c10.493 0 19-8.507 19-19s-8.507-19-19-19-19 8.507-19 19 8.507 19 19 19zm0 4c-12.703 0-23-10.297-23-23s10.297-23 23-23 23 10.297 23 23-10.297 23-23 23z"/><path fill="#6B4FBB" d="M109 56c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1zm4 0c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1zm4 0c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M97.5 40c0-5.8 4.698-10.5 10.494-10.5h10.012c5.796 0 10.494 4.7 10.494 10.5s-4.698 10.5-10.494 10.5h-10.012C102.198 50.5 97.5 45.8 97.5 40zm3 0c0 4.143 3.355 7.5 7.494 7.5h10.012c4.14 0 7.494-3.358 7.494-7.5 0-4.143-3.355-7.5-7.494-7.5h-10.012c-4.14 0-7.494 3.358-7.494 7.5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M109.255 42.406c-.195-.517.067-1.093.584-1.287.516-.196 1.093.066 1.287.583.29.774 1.033 1.297 1.873 1.297.855 0 1.608-.542 1.887-1.335.184-.52.755-.794 1.276-.61.52.183.794.754.61 1.275-.56 1.587-2.063 2.67-3.773 2.67-1.68 0-3.164-1.046-3.745-2.594zM105.5 40c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm15 0c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5z"/><path fill="#6B4FBB" d="M112 22h2c.552 0 1 .448 1 1s-.448 1-1 1h-2c-.552 0-1-.448-1-1s.448-1 1-1zm0 3h2c.552 0 1 .448 1 1s-.448 1-1 1h-2c-.552 0-1-.448-1-1s.448-1 1-1z" style="mix-blend-mode:multiply"/></g></g></svg>
...@@ -350,6 +350,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -350,6 +350,8 @@ constraints(ProjectUrlConstrainer.new) do
collection do collection do
post :bulk_update post :bulk_update
post :export_csv post :export_csv
get :service_desk ## EE-specific
end end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links' resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
......
...@@ -74,6 +74,7 @@ var config = { ...@@ -74,6 +74,7 @@ var config = {
protected_tags: './protected_tags', protected_tags: './protected_tags',
ee_protected_tags: 'ee/protected_tags', ee_protected_tags: 'ee/protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js', service_desk: './projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: './service_desk_issues/index.js',
repo: './repo/index.js', repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js', sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
......
...@@ -6,12 +6,13 @@ ...@@ -6,12 +6,13 @@
Service Desk is a module that allows your team to connect directly Service Desk is a module that allows your team to connect directly
with any external party through email right inside of GitLab; no external tools required. with any external party through email right inside of GitLab; no external tools required.
An ongoing conversation right where your software is built ensures that user feedback ends up directly where needed, An ongoing conversation right where your software is built ensures that user feedback ends
helping you build the right features to solve your user's real problems. up directly where it's needed, helping you build the right features to solve your users'
real problems.
Provide efficient email support to your customers, who can email bug reports, With Service Desk, you can provide efficient email support to your customers, who can now
feature requests, or any other general feedback directly into your GitLab project as a new issue. email you bug reports, feature requests, or general feedback that will all end up in your
In turn, your team can respond straight from the project. GitLab project as new issues. In turn, your team can respond straight from the project.
As Service Desk is built right into GitLab itself, the complexity and inefficiencies As Service Desk is built right into GitLab itself, the complexity and inefficiencies
of multiple tools and external integrations are eliminated, significantly shortening of multiple tools and external integrations are eliminated, significantly shortening
...@@ -23,12 +24,15 @@ For instance, let's assume you develop a game for iOS or Android. ...@@ -23,12 +24,15 @@ For instance, let's assume you develop a game for iOS or Android.
The codebase is hosted in your GitLab instance, built and deployed The codebase is hosted in your GitLab instance, built and deployed
with GitLab CI. with GitLab CI.
1. Offer email support to your paying customers, who can email you directly from their app Here's how Service Desk will work for you:
1. The email they send creates an issue in the appropriate project
1. Your team members reply to that issue thread to follow up with your customer 1. You'll provide a project-specific email address to your paying customers, who can email you directly from within the app
1. Each email they send creates an issue in the appropriate project
1. Your team members navigate to the Service Desk issue tracker, where they can see new support requests and respond inside associated issues
1. Your team communicates back and forth with the customer to understand the request
1. Your team starts working on implementing code to solve your customer's problem 1. Your team starts working on implementing code to solve your customer's problem
1. When your team finishes the implementation, their merged merge request will close the issue 1. When your team finishes the implementation, whereupon the merge request is merged and the issue is closed automatically
1. The customer will have been attended successfully through GitLab, without having real access to your GitLab instance 1. The customer will have been attended successfully via email, without having real access to your GitLab instance
1. Your team saved time by not having to leave GitLab (or setup any integrations) to follow up with your customer 1. Your team saved time by not having to leave GitLab (or setup any integrations) to follow up with your customer
## How it works ## How it works
...@@ -67,7 +71,9 @@ you can skip the step 1 below; you only need to enable it per project. ...@@ -67,7 +71,9 @@ you can skip the step 1 below; you only need to enable it per project.
checking to this service. checking to this service.
![Service Desk enabled](img/service_desk_enabled.png) ![Service Desk enabled](img/service_desk_enabled.png)
5. Service Desk is now enabled for this project! 5. Service Desk is now enabled for this project! You should be able to access it from your project's navigation **Issue submenu**:
![Service Desk Navigation Item](img/service_desk_nav_item.png)
## Using Service Desk ## Using Service Desk
...@@ -90,13 +96,18 @@ And any responses they send will be displayed in the issue itself. ...@@ -90,13 +96,18 @@ And any responses they send will be displayed in the issue itself.
### As a responder to the issue ### As a responder to the issue
For responders to the issue, everything works as usual. Messages from the end For responders to the issue, everything works as usual. They'll see a familiar looking
user will show as coming from the special Support Bot user, but apart from that, issue tracker, where they can see issues created via customer support requests and
filter and interact with them just like other GitLab issues.
![Service Desk Issue tracker](img/service_desk_issue_tracker.png)
Messages from the end user will show as coming from the special Support Bot user, but apart from that,
you can read and write comments as you normally do: you can read and write comments as you normally do:
![Service Desk issue thread](img/service_desk_thread.png) ![Service Desk issue thread](img/service_desk_thread.png)
> Note that the project's visibility (private, internal, public) does not affect Service Desk. > Note that the project's visibility (private, internal, public) does not affect Service Desk.
[ee-149]: https://gitlab.com/gitlab-org/gitlab-ee/issues/149 "Service Desk with email" [ee-149]: https://gitlab.com/gitlab-org/gitlab-ee/issues/149 "Service Desk with email"
[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition landing page" [ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition landing page"
......
.service-desk-issues {
.empty-state {
max-width: 450px;
text-align: center;
}
.non-empty-state {
text-align: left;
padding-bottom: $gl-padding-top;
border-bottom: 1px solid $border-color;
.service-desk-graphic {
margin-top: $gl-padding;
}
.media-body {
margin-top: $gl-padding-top;
margin-left: $gl-padding;
}
}
.turn-on-btn-container {
margin-top: $gl-padding-top;
}
}
...@@ -5,6 +5,18 @@ module EE ...@@ -5,6 +5,18 @@ module EE
prepended do prepended do
before_action :check_export_issues_available!, only: [:export_csv] before_action :check_export_issues_available!, only: [:export_csv]
before_action :check_service_desk_available!, only: [:service_desk]
before_action :set_issues_index, only: [:index, :service_desk]
skip_before_action :issue, only: [:service_desk]
end
def service_desk
if params[:assignee_id].present?
assignee = User.find_by_id(params[:assignee_id])
@users.push(assignee) if assignee
end
@users.push(::User.support_bot)
end end
def export_csv def export_csv
...@@ -14,6 +26,8 @@ module EE ...@@ -14,6 +26,8 @@ module EE
redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.") redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.")
end end
private
def issue_params_attributes def issue_params_attributes
attrs = super attrs = super
attrs.unshift(:weight) if project.feature_available?(:issue_weights) attrs.unshift(:weight) if project.feature_available?(:issue_weights)
...@@ -25,8 +39,17 @@ module EE ...@@ -25,8 +39,17 @@ module EE
params = super params = super
params.reject! { |key| key == 'weight' } unless project.feature_available?(:issue_weights) params.reject! { |key| key == 'weight' } unless project.feature_available?(:issue_weights)
if service_desk?
params.reject! { |key| key == 'author_username' || key == 'author_id' }
params[:author_id] = ::User.support_bot
end
params params
end end
def service_desk?
action_name == 'service_desk'
end
end end
end end
end end
- 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'
%div{ class: "#{callout_selector}" }
.service-desk-graphic
= render svg_path
.media-body
%h5 Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab.
- if 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 !service_desk_enabled
.turn-on-btn-container
= link_to "Turn on Service Desk", edit_project_path(@project), class: 'btn btn-new btn-inverted'
- @no_container = true
- @can_bulk_update = false
- page_title "Service Desk"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'service_desk_issues'
= webpack_bundle_tag 'issues'
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
- support_bot_attrs = User.support_bot.to_json(only: [:id, :name, :username, :avatar_url])
%div{ class: "#{container_class} js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs } }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls.visible-xs
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
= render 'shared/issuable/search_bar', type: :issues
- if @issues.present?
= render 'service_desk_info_content'
.issues-holder
= render 'projects/issues/issues', empty_state_path: 'service_desk_info_content'
require('spec_helper') require('spec_helper')
describe Projects::IssuesController do describe Projects::IssuesController do
let(:namespace) { create(:namespace) } let(:namespace) { create(:group, :public) }
let(:project) { create(:project_empty_repo, namespace: namespace) } let(:project) { create(:project_empty_repo, :public, namespace: namespace) }
let(:user) { create(:user) }
let(:viewer) { user }
let(:issue) { create(:issue, project: project) }
describe 'POST export_csv' do describe 'POST export_csv' do
let(:user) { create(:user) }
let(:viewer) { user }
let(:issue) { create(:issue, project: project) }
let(:globally_licensed) { false } let(:globally_licensed) { false }
before do before do
...@@ -61,6 +61,7 @@ describe Projects::IssuesController do ...@@ -61,6 +61,7 @@ describe Projects::IssuesController do
context 'licensed by namespace' do context 'licensed by namespace' do
let(:globally_licensed) { true } let(:globally_licensed) { true }
let(:namespace) { create(:group, :private, plan: Namespace::BRONZE_PLAN) } let(:namespace) { create(:group, :private, plan: Namespace::BRONZE_PLAN) }
let(:project) { create(:project, namespace: namespace) }
before do before do
stub_application_setting(check_namespace_plan: true) stub_application_setting(check_namespace_plan: true)
...@@ -192,4 +193,52 @@ describe Projects::IssuesController do ...@@ -192,4 +193,52 @@ describe Projects::IssuesController do
end end
end end
end end
describe 'GET service_desk' do
def get_service_desk(extra_params = {})
get :service_desk, extra_params.merge(namespace_id: project.namespace, project_id: project)
end
context 'when Service Desk is available on the project' do
let(:support_bot) { User.support_bot }
let(:other_user) { create(:user) }
let!(:service_desk_issue_1) { create(:issue, project: project, author: support_bot) }
let!(:service_desk_issue_2) { create(:issue, project: project, author: support_bot, assignees: [other_user]) }
let!(:other_user_issue) { create(:issue, project: project, author: other_user) }
before do
stub_licensed_features(service_desk: true)
end
it 'adds an author filter for the support bot user' do
get_service_desk
expect(assigns(:issues)).to contain_exactly(service_desk_issue_1, service_desk_issue_2)
end
it 'does not allow any other author to be set' do
get_service_desk(author_username: other_user.username)
expect(assigns(:issues)).to contain_exactly(service_desk_issue_1, service_desk_issue_2)
end
it 'supports other filters' do
get_service_desk(assignee_username: other_user.username)
expect(assigns(:issues)).to contain_exactly(service_desk_issue_2)
end
end
context 'when Service Desk is not available on the project' do
before do
stub_licensed_features(service_desk: false)
end
it 'returns a 404' do
get_service_desk
expect(response).to have_http_status(404)
end
end
end
end end
require 'spec_helper'
describe 'Service Desk Issue Tracker', js: true do
let(:project) { create(:project, :private, service_desk_enabled: true) }
let(:user) { create(:user) }
before do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk) { true }
allow(Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
project.add_master(user)
sign_in(user)
end
describe 'navigation to service desk' do
before do
visit project_path(project)
find('.sidebar-top-level-items .shortcuts-issues').click
find('.sidebar-sub-level-items a[title="Service Desk"]').click
end
it 'can navigate to the service desk from link in the sidebar' do
expect(page).to have_content('Use Service Desk to connect with your users')
end
end
describe 'issues list' do
context 'when service desk has not been activated' do
let(:project_without_service_desk) { create(:project, :private, service_desk_enabled: false) }
describe 'service desk info content' do
before do
project_without_service_desk.add_master(user)
visit service_desk_project_issues_path(project_without_service_desk)
end
it 'displays the large info box' do
expect(page).to have_css('.empty-state')
end
it 'has a link to the documentation' do
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
end
it 'does show a button configure service desk' do
expect(page).to have_link('Turn on Service Desk')
end
end
end
context 'when service desk has been activated' do
before do
end
context 'when there are no issues' do
describe 'service desk info content' do
before do
visit service_desk_project_issues_path(project)
end
it 'displays the large info box' do
expect(page).to have_css('.empty-state')
end
it 'has a link to the documentation' do
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
end
it 'does not show a button configure service desk' do
expect(page).not_to have_link('Turn on Service Desk')
end
it 'shows the service desk email address' do
expect(page).to have_content(project.service_desk_address)
end
end
end
context 'when there are issues' do
let(:support_bot) { User.support_bot }
let(:other_user) { create(:user) }
let!(:service_desk_issue) { create(:issue, project: project, author: support_bot) }
let!(:other_user_issue) { create(:issue, project: project, author: other_user) }
describe 'service desk info content' do
before do
visit service_desk_project_issues_path(project)
end
it 'displays the small info box' do
expect(page).to have_css('.non-empty-state')
end
it 'has a link to the documentation' do
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
end
it 'does not show a button configure service desk' do
expect(page).not_to have_link('Turn on Service Desk')
end
it 'shows the service desk email address' do
expect(page).to have_content(project.service_desk_address)
end
end
describe 'issues list' do
before do
visit service_desk_project_issues_path(project)
end
it 'only displays issues created by support bot' do
expect(page).to have_selector('.issues-list .issue', count: 1)
end
end
describe 'search box' do
before do
visit service_desk_project_issues_path(project)
end
it 'displays the support bot author token' do
author_token = find('.filtered-search-token .value')
expect(author_token).to have_content('Support Bot')
end
it 'support bot author token cannot be deleted' do
find('.input-token .filtered-search').native.send_key(:backspace)
expect(page).to have_selector('.js-visual-token', count: 1)
end
end
end
end
end
end
...@@ -411,4 +411,26 @@ describe('Filtered Search Manager', () => { ...@@ -411,4 +411,26 @@ describe('Filtered Search Manager', () => {
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
}); });
}); });
describe('getAllParams', () => {
beforeEach(() => {
this.paramsArr = ['key=value', 'otherkey=othervalue'];
initializeManager();
});
it('correctly modifies params when custom modifier is passed', () => {
const modifedParams = manager.getAllParams.call({
modifyUrlParams: paramsArr => paramsArr.reverse(),
}, [].concat(this.paramsArr));
expect(modifedParams[0]).toBe(this.paramsArr[1]);
});
it('does not modify params when no custom modifier is passed', () => {
const modifedParams = manager.getAllParams.call({}, this.paramsArr);
expect(modifedParams[1]).toBe(this.paramsArr[1]);
});
});
}); });
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