Commit 811c0255 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-07-28

parents 745db030 e09a5a90
......@@ -146,6 +146,8 @@ import AuditLogs from './audit_logs';
.init();
}
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
switch (page) {
case 'profiles:preferences:show':
initExperimentalFlags();
......@@ -162,7 +164,7 @@ import AuditLogs from './audit_logs';
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
......@@ -190,11 +192,17 @@ import AuditLogs from './audit_logs';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
case 'groups:issues':
case 'groups:merge_requests':
new ProjectSelect();
initLegacyFilters();
break;
case 'groups:issues':
if (filteredSearchEnabled) {
const filteredSearchManager = new gl.FilteredSearchManager('issues');
filteredSearchManager.setup();
}
new ProjectSelect();
break;
case 'dashboard:todos:index':
new Todos();
break;
......
......@@ -11,6 +11,16 @@ const Ajax = {
if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data);
},
preprocessing: function preprocessing(config, data) {
let results = data;
if (config.preprocessing && !data.preprocessed) {
results = config.preprocessing(data);
AjaxCache.override(config.endpoint, results);
}
return results;
},
init: function init(hook) {
var self = this;
self.destroyed = false;
......@@ -31,7 +41,8 @@ const Ajax = {
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
AjaxCache.retrieve(config.endpoint)
return AjaxCache.retrieve(config.endpoint)
.then(self.preprocessing.bind(null, config))
.then((data) => self._loadData(data, config, self))
.catch(config.onError);
},
......
......@@ -6,7 +6,7 @@ import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
constructor(options = {}) {
const { input, endpoint, symbol } = options;
const { input, endpoint, symbol, preprocessing } = options;
super(options);
this.symbol = symbol;
this.config = {
......@@ -14,6 +14,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
endpoint,
method: 'setData',
loadingTemplate: this.loadingTemplate,
preprocessing,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
......
......@@ -75,6 +75,66 @@ class DropdownUtils {
return updatedItem;
}
static mergeDuplicateLabels(dataMap, newLabel) {
const updatedMap = dataMap;
const key = newLabel.title;
const hasKeyProperty = Object.prototype.hasOwnProperty.call(updatedMap, key);
if (!hasKeyProperty) {
updatedMap[key] = newLabel;
} else {
const existing = updatedMap[key];
if (!existing.multipleColors) {
existing.multipleColors = [existing.color];
}
existing.multipleColors.push(newLabel.color);
}
return updatedMap;
}
static duplicateLabelColor(labelColors) {
const colors = labelColors;
const spacing = 100 / colors.length;
// Reduce the colors to 4
colors.length = Math.min(colors.length, 4);
const color = colors.map((c, i) => {
const percentFirst = Math.floor(spacing * i);
const percentSecond = Math.floor(spacing * (i + 1));
return `${c} ${percentFirst}%, ${c} ${percentSecond}%`;
}).join(', ');
return `linear-gradient(${color})`;
}
static duplicateLabelPreprocessing(data) {
const results = [];
const dataMap = {};
data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap));
Object.keys(dataMap)
.forEach((key) => {
const label = dataMap[key];
if (label.multipleColors) {
label.color = DropdownUtils.duplicateLabelColor(label.multipleColors);
label.text_color = '#000000';
}
results.push(label);
});
results.preprocessed = true;
return results;
}
static setDataValueIfSelected(filter, selected) {
const dataValue = selected.getAttribute('data-value');
......
......@@ -58,6 +58,7 @@ class FilteredSearchDropdownManager {
extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json`,
symbol: '~',
preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing,
},
element: this.container.querySelector('#js-dropdown-label'),
},
......
......@@ -28,13 +28,13 @@ class FilteredSearchManager {
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.projectFullPath : 'project';
const fullPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.fullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
......
......@@ -58,29 +58,54 @@ class FilteredSearchVisualTokens {
`;
}
static setTokenStyle(tokenContainer, backgroundColor, textColor) {
const token = tokenContainer;
// Labels with linear gradient should not override default background color
if (backgroundColor.indexOf('linear-gradient') === -1) {
token.style.backgroundColor = backgroundColor;
}
token.style.color = textColor;
if (textColor === '#FFFFFF') {
const removeToken = token.querySelector('.remove-token');
removeToken.classList.add('inverted');
}
return token;
}
static preprocessLabel(labelsEndpoint, labels) {
let processed = labels;
if (!labels.preprocessed) {
processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels);
AjaxCache.override(labelsEndpoint, processed);
processed.preprocessed = true;
}
return processed;
}
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`;
return AjaxCache.retrieve(labelsEndpoint)
.then((labels) => {
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
if (!matchingLabel) {
return;
}
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
.then((labels) => {
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
const tokenValueStyle = tokenValueContainer.style;
tokenValueStyle.backgroundColor = matchingLabel.color;
tokenValueStyle.color = matchingLabel.text_color;
if (!matchingLabel) {
return;
}
if (matchingLabel.text_color === '#FFFFFF') {
const removeToken = tokenValueContainer.querySelector('.remove-token');
removeToken.classList.add('inverted');
}
})
.catch(() => new Flash('An error occurred while fetching label colors.'));
FilteredSearchVisualTokens
.setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color);
})
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
......
......@@ -3,6 +3,7 @@
/* global ListLabel */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
(function() {
this.LabelsSelect = (function() {
......@@ -218,18 +219,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
}
}
if (label.duplicate) {
spacing = 100 / label.color.length;
// Reduce the colors to 4
label.color = label.color.filter(function(color, i) {
return i < 4;
});
color = _.map(label.color, function(color, i) {
var percentFirst, percentSecond;
percentFirst = Math.floor(spacing * i);
percentSecond = Math.floor(spacing * (i + 1));
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
}).join(',');
color = "linear-gradient(" + color + ")";
color = gl.DropdownUtils.duplicateLabelColor(label.color);
}
else {
if (label.color != null) {
......
......@@ -6,6 +6,10 @@ class AjaxCache extends Cache {
this.pendingRequests = { };
}
override(endpoint, data) {
this.internalStorage[endpoint] = data;
}
retrieve(endpoint, forceRetrieve) {
if (this.hasData(endpoint) && !forceRetrieve) {
return Promise.resolve(this.get(endpoint));
......
......@@ -143,10 +143,19 @@ $new-sidebar-width: 220px;
white-space: nowrap;
a {
display: block;
display: flex;
align-items: center;
padding: 12px 16px;
color: $inactive-color;
}
svg {
fill: $inactive-color;
}
}
.nav-item-name {
flex: 1;
}
li.active {
......@@ -156,11 +165,25 @@ $new-sidebar-width: 220px;
color: $active-color;
font-weight: 700;
}
svg {
fill: $active-color;
}
}
@media (max-width: $screen-xs-max) {
left: (-$new-sidebar-width);
}
.nav-icon-container {
display: flex;
margin-right: 8px;
svg {
height: 16px;
width: 16px;
}
}
}
.with-performance-bar .nav-sidebar {
......@@ -173,7 +196,7 @@ $new-sidebar-width: 220px;
> li {
a {
padding: 8px 16px 8px 24px;
padding: 8px 16px 8px 50px;
&:hover,
&:focus {
......@@ -198,7 +221,6 @@ $new-sidebar-width: 220px;
.sidebar-top-level-items {
> li {
.badge {
float: right;
background-color: $inactive-badge-background;
color: $inactive-color;
}
......@@ -220,6 +242,10 @@ $new-sidebar-width: 220px;
background-color: $hover-background;
color: $hover-color;
svg {
fill: $hover-color;
}
.badge {
background-color: $indigo-500;
color: $hover-color;
......
......@@ -10,8 +10,10 @@ module EE
private
def search_multiple_assignees?(type)
context = @project.presence || @group
type == :issues &&
@project.feature_available?(:multiple_issue_assignees)
context.feature_available?(:multiple_issue_assignees)
end
end
end
......@@ -137,15 +137,23 @@ module SearchHelper
end
def search_filter_input_options(type)
{
opts = {
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
'project-id' => @project.id,
'username-params' => @users.to_json(only: [:id, :username]),
'base-endpoint' => project_path(@project)
'username-params' => @users.to_json(only: [:id, :username])
}
}
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
else
# Group context
opts[:data]['base-endpoint'] = group_canonical_path(@group)
end
opts
end
# Sanitize a HTML field for search display. Most tags are stripped out and the
......
......@@ -4,6 +4,11 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'issues'
- if show_new_nav? && group_issues_exists
- content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
......@@ -20,7 +25,7 @@
Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue"
= render 'shared/issuable/filter', type: :issues
= render 'shared/issuable/search_bar', type: :issues
.row-content-block.second-block
Only issues from the
......
......@@ -10,7 +10,9 @@
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
.nav-icon-container
= custom_icon('overview')
%span.nav-item-name
Overview
%ul.sidebar-sub-level-items
......@@ -45,7 +47,9 @@
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles audit_logs)) do
= link_to admin_conversational_development_index_path, title: 'Monitoring' do
%span
.nav-icon-container
= custom_icon('monitoring')
%span.nav-item-name
Monitoring
%ul.sidebar-sub-level-items
......@@ -80,67 +84,93 @@
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
.nav-icon-container
= custom_icon('messages')
%span.nav-item-name
Messages
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
.nav-icon-container
= custom_icon('system_hooks')
%span.nav-item-name
System Hooks
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
%span
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
Applications
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
.nav-icon-container
= custom_icon('abuse_reports')
%span.nav-item-name
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
= nav_link(controller: :licenses) do
= link_to admin_license_path, title: 'License' do
.nav-icon-container
= custom_icon('license')
%span
License
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
Spam Logs
= nav_link(controller: :push_rules) do
= link_to admin_push_rule_path, title: 'Push Rules' do
.nav-icon-container
= custom_icon('push_rules')
%span
Push Rules
= nav_link(controller: :geo_nodes) do
= link_to admin_geo_nodes_path, title: 'Geo Nodes' do
.nav-icon-container
= custom_icon('geo_nodes')
%span
Geo Nodes
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
%span
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
Deploy Keys
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
%span
.nav-icon-container
= custom_icon('service_templates')
%span.nav-item-name
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
%span
.nav-icon-container
= custom_icon('labels')
%span.nav-item-name
Labels
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
%span
.nav-icon-container
= custom_icon('appearance')
%span.nav-item-name
Appearance
%li.divider
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
%span
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
Settings
......@@ -11,7 +11,9 @@
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'About group' do
%span
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
About
%ul.sidebar-sub-level-items
......@@ -33,10 +35,12 @@
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count)
Issues
%span.badge.count= number_with_delimiter(issues.count)
%ul.sidebar-sub-level-items
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
......@@ -56,18 +60,24 @@
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
Merge Requests
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
.nav-icon-container
= custom_icon('members')
%span.nav-item-name
Members
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]) do
= link_to edit_group_path(@group), title: 'Settings' do
%span
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items
= nav_link(path: 'groups#edit') do
......
......@@ -10,42 +10,60 @@
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
%span
.nav-icon-container
= custom_icon('profile')
%span.nav-item-name
Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
%span
.nav-icon-container
= custom_icon('account')
%span.nav-item-name
Account
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
%span
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
Applications
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path, title: 'Chat' do
%span
.nav-icon-container
= custom_icon('chat')
%span.nav-item-name
Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
.nav-icon-container
= custom_icon('access_tokens')
%span.nav-item-name
Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
.nav-icon-container
= custom_icon('emails')
%span.nav-item-name
Emails
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
%span
.nav-icon-container
= custom_icon('lock')
%span.nav-item-name
Password
= nav_link(controller: :notifications) do
= link_to profile_notifications_path, title: 'Notifications' do
%span
.nav-icon-container
= custom_icon('notifications')
%span.nav-item-name
Notifications
= nav_link(controller: :keys) do
= link_to profile_keys_path, title: 'SSH Keys' do
%span
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
SSH Keys
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path, title: 'GPG Keys' do
......@@ -53,9 +71,13 @@
GPG Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
.nav-icon-container
= custom_icon('preferences')
%span.nav-item-name
Preferences
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Authentication log' do
%span
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
Authentication log
......@@ -12,7 +12,9 @@
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= link_to project_path(@project), title: 'About project', class: 'shortcuts-project' do
%span
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
About
%ul.sidebar-sub-level-items
......@@ -32,7 +34,9 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
= link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
.nav-icon-container
= custom_icon('doc_text')
%span.nav-item-name
Repository
%ul.sidebar-sub-level-items
......@@ -71,16 +75,20 @@
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
%span
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
Registry
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
- if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
Issues
- if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
%ul.sidebar-sub-level-items
- if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
......@@ -115,14 +123,18 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
Merge Requests
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
Pipelines
%ul.sidebar-sub-level-items
......@@ -159,19 +171,25 @@
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
%span
.nav-icon-container
= custom_icon('wiki')
%span.nav-item-name
Wiki
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
%span
.nav-icon-container
= custom_icon('snippets')
%span.nav-item-name
Snippets
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
Settings
%ul.sidebar-sub-level-items
......@@ -207,9 +225,11 @@
- else
= nav_link(path: %w[members#show]) do
= link_to project_settings_members_path(@project), title: 'Settings', class: 'shortcuts-tree' do
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('members')
%span
Settings
Members
-# Shortcut to Project > Activity
%li.hidden
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m13 2h-10c-1.7 0-3 1.3-3 3v6c0 1.7 1.3 3 3 3h10c1.7 0 3-1.3 3-3v-6c0-1.7-1.3-3-3-3m1 9c0 .6-.4 1-1 1h-10c-.6 0-1-.4-1-1v-6c0-.6.4-1 1-1h10c.6 0 1 .4 1 1v6"/><circle cx="4" cy="8" r="1"/><circle cx="8" cy="8" r="1"/><circle cx="12" cy="8" r="1"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m6.8 8c-.3 0-.5 0-.8 0-5 0-6 2.7-6 4.5s.1 2.5 6 2.5c.6 0 1.1 0 1.5 0-1-1.1-1.5-2.5-1.5-4 0-1.1.3-2.1.8-3"/><circle cx="6" cy="4" r="3"/><path d="m15.9 11.5l-.9-.6c0-.3-.1-.7-.2-.9l.6-.9c.1-.1.1-.2 0-.3l-.4-.5c-.1-.1-.2-.1-.3-.1l-.9.4c-.3-.2-.5-.3-.9-.4l-.3-1c0-.1-.1-.2-.2-.2h-.6c-.1 0-.2.1-.2.2l-.3 1c-.3.1-.6.2-.9.4l-1.1-.4c-.1 0-.2 0-.3.1l-.4.5c0 .1 0 .2 0 .3l.6.9c-.1.3-.2.6-.2.9l-.9.5c-.1.1-.1.2-.1.3l.1.6c0 .1.1.2.2.2l1.1.1c.1.2.3.4.5.6l-.2 1.2c0 .1 0 .2.1.3l.6.3c.1 0 .2 0 .3-.1l.9-.9c.2 0 .4 0 .6 0l.9.9c.1.1.2.1.3 0l.6-.3c.1 0 .2-.2.1-.3l-.1-1.1c.2-.2.4-.4.5-.6l1.1-.1c.1 0 .2-.1.2-.2l.1-.6c.1-.1.1-.2 0-.2m-3.9.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858-.411.022-.744.026-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023.172-.009.332-.02.478-.035-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.414 12l-3.707 3.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm0-2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3z"/><path d="M3.212 4L8 8.31 12.788 4H3.212zm6.126 5.796a2 2 0 0 1-2.676 0L.183 3.965A3.001 3.001 0 0 1 3 2h10c1.293 0 2.395.818 2.817 1.965l-6.48 5.83z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4"/><path d="m7.3 12.7c.4.4 1 .3 1.4-.1 2.9-3.1 4.3-5.6 4.3-7.3 0-2.9-2.2-5.3-5-5.3s-5 2.4-5 5.3c0 1.7 1.4 4.2 4.3 7.4m.7-10.7c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2 0-1.9 1.4-3.3 3-3.3"/><circle cx="8" cy="5" r="1"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></svg>
<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16" class="gitlab-icon">
<path fill="#7E7C7C" d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2"></path>
<path fill="#7E7C7C" d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z"></path>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7.574 6.689a4.002 4.002 0 0 1 6.275-4.861 4 4 0 0 1-4.86 6.275l-2.21 2.21.706.707a1 1 0 0 1-1.414 1.415l-.707-.708-.707.708.707.707a1 1 0 0 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.415-1.414l5.746-5.746zm2.033-.618a2 2 0 1 0 2.828-2.829 2 2 0 0 0-2.828 2.829z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667z"/><path d="M.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m8 9c-.6 0-1 .4-1 1v1c0 .6.4 1 1 1s1-.4 1-1v-1c0-.6-.4-1-1-1"/><path d="m12 5v-1c0-2.2-1.8-4-4-4s-4 1.8-4 4v1c-1.7 0-3 1.3-3 3v5c0 1.7 1.3 3 3 3h8c1.7 0 3-1.3 3-3v-5c0-1.7-1.3-3-3-3m-6-1c0-1.1.9-2 2-2s2 .9 2 2v1h-4v-1m7 9c0 .6-.4 1-1 1h-8c-.6 0-1-.4-1-1v-5c0-.6.4-1 1-1h8c.6 0 1 .4 1 1v5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2 2v3h3V2H2zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm9 2v3h3V2h-3zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zM2 11v3h3v-3H2zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm9 2v3h3v-3h-3zm0-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16"><path d="m8 0c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8m0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6"/><circle cx="12.5" cy="9.5" r=".5"/><circle cx="12.5" cy="6.5" r=".5"/><circle cx="10.5" cy="12.5" r=".5"/><circle cx="10.5" cy="3.5" r=".5"/><circle cx="5.5" cy="12.5" r=".5"/><circle cx="5.5" cy="3.5" r=".5"/><circle cx="3.5" cy="9.5" r=".5"/><circle cx="3.5" cy="6.5" r=".5"/><path d="m9 7.2c0 0 0-.1 0-.2v-1.9c0-.1 0-.1-.1-.2l-.8-.8c0 0-.1 0-.1 0l-.9.8c-.1.1-.1.1-.1.2v1.9c0 .1 0 .2 0 .2-.6.4-1 1-1 1.8 0 1.1.9 2 2 2s2-.9 2-2c0-.8-.4-1.4-1-1.8m-1 2.8c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2zm10 5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zm-5 5h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2z" fill-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></svg>
<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16">
<path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E" fill-rule="evenodd"></path>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5.268 9a2 2 0 0 1 3.464 0H10a1 1 0 0 1 0 2H8.732a2 2 0 0 1-3.464 0H4a1 1 0 0 1 0-2h1.268zM6 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 6 5.515V2zM13.749.094A3.001 3.001 0 0 1 16 3v10a3.001 3.001 0 0 1-2.251 2.906A3.989 3.989 0 0 0 15 13V3c0-1.144-.48-2.177-1.251-2.906zM3 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918c.594.181 1.15.452 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 1 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 1 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 8 6.191V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></svg>
- type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil)
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
......@@ -22,7 +23,7 @@
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.js-filtered-search-history-dropdown{ data: { full_path: full_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
......
---
title: Fix accessing individual files on Object Storage
merge_request:
author:
---
title: Add icons to contextual sidebars
merge_request:
author:
......@@ -426,8 +426,6 @@ Here are some things to keep in mind regarding test performance:
- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
- Use `create(:empty_project)` instead of `create(:project)` when you don't need
the underlying Git repository. Filesystem operations are slow!
- Don't mark a feature as requiring JavaScript (through `@javascript` in
Spinach or `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
......
......@@ -57,6 +57,10 @@ of that milestone and the issues/merge requests count that it shares across the
In addition to that you will be able to filter issues or merge requests by group milestones in all projects that belongs to the milestone group.
## Milestone promotion
You will be able to promote a project milestone to a group milestone [in the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/35833).
## Special milestone filters
In addition to the milestones that exist in the project or group, there are some
......@@ -87,3 +91,7 @@ total merge requests and issues.
are visual representations of the progress of completing a milestone.
![burndown chart](img/burndown_chart.png)
Burndown charts are only available for project milestones currently. They will be available for group milestones [in the future](https://gitlab.com/gitlab-org/gitlab-ee/issues/3064).
[Quick actions](../quick_actions.md) are available for assigning and removing project milestones only. [In the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/34778), this will also apply to group milestones.
\ No newline at end of file
......@@ -100,7 +100,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
group = owned_group
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create(:empty_project, path: path, group: group)
project = create(:project, path: path, group: group)
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
......
......@@ -14,12 +14,12 @@ class Spinach::Features::GroupHooks < Spinach::FeatureSteps
end
step 'I own project "Shop" in group "Sourcing"' do
@project = create(:project,
@project = create(:project, :repository,
name: 'Shop', group: @group)
end
step 'I own empty project "Empty Shop" in group "Sourcing"' do
@project = create(:empty_project,
@project = create(:project,
name: 'Shop', group: @group)
end
......
......@@ -15,7 +15,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has a public project "Public-project"' do
group = owned_group
@project = create :empty_project, :public,
@project = create :project, :public,
group: group,
name: "Public-project"
end
......@@ -132,7 +132,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
@archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project")
@archived_project = create(:project, :archived, namespace: group, path: "archived-project")
end
step 'I should see "archived" label' do
......
......@@ -47,11 +47,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'other projects have deploy keys' do
@second_project = create(:empty_project, namespace: create(:group))
@second_project = create(:project, namespace: create(:group))
@second_project.team << [current_user, :master]
create(:deploy_keys_project, project: @second_project)
@third_project = create(:empty_project, namespace: create(:group))
@third_project = create(:project, namespace: create(:group))
@third_project.team << [current_user, :master]
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
end
......
......@@ -16,7 +16,7 @@ class Spinach::Features::ProjectSearch < Spinach::FeatureSteps
end
step 'project has all data available for the search' do
@project = create :project
@project = create :project, :repository
@project.team << [current_user, :master]
@issue = create :issue, title: 'bla-bla initial', project: @project
......
......@@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
include SharedProject
step 'public project "Community"' do
create(:empty_project, :public, name: 'Community')
create(:project, :public, name: 'Community')
end
step 'private project "Enterprise"' do
create(:empty_project, :private, name: 'Enterprise')
create(:project, :private, name: 'Enterprise')
end
step 'I visit project "Community" page' do
......
......@@ -34,7 +34,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I own project "Website"' do
@project = create(:empty_project, name: "Website", namespace: @user.namespace)
@project = create(:project, name: "Website", namespace: @user.namespace)
@project.team << [@user, :master]
end
......@@ -68,7 +68,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
os_group = create(:group, name: 'OpenSource')
create(:empty_project, group: os_group)
create(:project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
os_group.add_owner(@os_user1)
......
......@@ -54,7 +54,7 @@ module SharedProject
# Create an empty project without caring about the name
step 'I own an empty project' do
@project = create(:empty_project,
@project = create(:project,
name: 'Empty Project', namespace: @user.namespace)
@project.team << [@user, :master]
end
......@@ -276,7 +276,7 @@ module SharedProject
def user_owns_project(user_name:, project_name:, visibility: :private)
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
project ||= create(:empty_project, visibility, name: project_name, namespace: user.namespace)
project ||= create(:project, visibility, name: project_name, namespace: user.namespace)
project.team << [user, :master]
end
end
......@@ -55,6 +55,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
@contributed_project ||= create(:empty_project, :public)
@contributed_project ||= create(:project, :public)
end
end
......@@ -45,7 +45,12 @@ module Gitlab
existing = ::Geo::ProjectRegistry.where(project_id: project_ids).pluck(:project_id)
missing_projects = project_ids - existing
Rails.logger.debug("Missing projects: #{missing_projects}")
Gitlab::Geo::Logger.debug(
class: self.class.name,
message: "Missing projects",
projects: missing_projects,
project_count: missing_projects.count)
missing_projects.each do |id|
::Geo::ProjectRegistry.create(project_id: id)
end
......@@ -91,6 +96,15 @@ module Gitlab
registry.resync_wiki = true
end
Gitlab::Geo::Logger.info(
class: self.class.name,
message: "Repository update",
cursor_delay_s: (Time.now - updated_event.created_at).to_f.round(3),
project_id: updated_event.project_id,
source: updated_event.source,
resync_repository: registry.resync_repository,
resync_wiki: registry.resync_wiki)
registry.save!
end
......
......@@ -126,8 +126,16 @@ module Gitlab
end
def send_artifacts_entry(build, entry)
file = build.artifacts_file
archive =
if file.file_storage?
file.path
else
file.url
end
params = {
'Archive' => build.artifacts_file.path,
'Archive' => archive,
'Entry' => Base64.encode64(entry.path)
}
......
......@@ -4,7 +4,7 @@ describe Admin::ApplicationSettingsController do
include StubENV
let(:group) { create(:group) }
let(:project) { create(:empty_project, namespace: group) }
let(:project) { create(:project, namespace: group) }
let(:admin) { create(:admin) }
let(:user) { create(:user)}
......
......@@ -29,8 +29,8 @@ describe Admin::DashboardController do
it 'does not retrieve projects that are pending deletion' do
sign_in(create(:admin))
project = create(:empty_project)
pending_delete_project = create(:empty_project, pending_delete: true)
project = create(:project)
pending_delete_project = create(:project, pending_delete: true)
get :index
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Admin::GroupsController do
let(:group) { create(:group) }
let(:project) { create(:empty_project, namespace: group) }
let(:project) { create(:project, namespace: group) }
let(:admin) { create(:admin) }
before do
......
require 'spec_helper'
describe Admin::ProjectsController do
let!(:project) { create(:empty_project, :public) }
let!(:project) { create(:project, :public) }
before do
sign_in(create(:admin))
......
......@@ -8,7 +8,7 @@ describe Admin::ServicesController do
end
describe 'GET #edit' do
let!(:project) { create(:empty_project) }
let!(:project) { create(:project) }
Service.available_services_names.each do |service_name|
context "#{service_name}" do
......@@ -27,7 +27,7 @@ describe Admin::ServicesController do
end
describe "#update" do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let!(:service) do
RedmineService.create(
project: project,
......
......@@ -9,7 +9,7 @@ describe Admin::UsersController do
end
describe 'DELETE #user with projects' do
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
before do
......
require 'spec_helper'
describe AutocompleteController do
let!(:project) { create(:empty_project) }
let!(:project) { create(:project) }
let!(:user) { create(:user) }
context 'GET users' do
......@@ -105,7 +105,7 @@ describe AutocompleteController do
end
context 'non-member login for public project' do
let!(:project) { create(:empty_project, :public) }
let!(:project) { create(:project, :public) }
before do
sign_in(non_member)
......@@ -167,7 +167,7 @@ describe AutocompleteController do
end
context 'unauthenticated user' do
let(:public_project) { create(:empty_project, :public) }
let(:public_project) { create(:project, :public) }
let(:body) { JSON.parse(response.body) }
describe 'GET #users with public project' do
......@@ -271,8 +271,8 @@ describe AutocompleteController do
end
context 'GET projects' do
let(:authorized_project) { create(:empty_project) }
let(:authorized_search_project) { create(:empty_project, name: 'rugged') }
let(:authorized_project) { create(:project) }
let(:authorized_search_project) { create(:project, name: 'rugged') }
before do
sign_in(user)
......@@ -329,8 +329,8 @@ describe AutocompleteController do
context 'authorized projects apply limit' do
before do
authorized_project2 = create(:empty_project)
authorized_project3 = create(:empty_project)
authorized_project2 = create(:project)
authorized_project3 = create(:project)
authorized_project.add_master(user)
authorized_project2.add_master(user)
......@@ -355,8 +355,8 @@ describe AutocompleteController do
context 'authorized projects with offset' do
before do
authorized_project2 = create(:empty_project)
authorized_project3 = create(:empty_project)
authorized_project2 = create(:project)
authorized_project3 = create(:project)
authorized_project.add_master(user)
authorized_project2.add_master(user)
......
require 'spec_helper'
describe Dashboard::LabelsController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
......@@ -11,7 +11,7 @@ describe Dashboard::LabelsController do
end
describe "#index" do
let!(:unrelated_label) { create(:label, project: create(:empty_project, :public)) }
let!(:unrelated_label) { create(:label, project: create(:project, :public)) }
it 'returns global labels for projects the user has a relationship with' do
get :index, format: :json
......
require 'spec_helper'
describe Dashboard::MilestonesController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_milestone) { create(:milestone, project: project) }
let(:milestone) do
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Dashboard::TodosController do
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:todo_service) { TodoService.new }
before do
......@@ -14,7 +14,7 @@ describe Dashboard::TodosController do
describe 'GET #index' do
context 'project authorization' do
it 'renders 404 when user does not have read access on given project' do
unauthorized_project = create(:empty_project, :private)
unauthorized_project = create(:project, :private)
get :index, project_id: unauthorized_project.id
......@@ -34,7 +34,7 @@ describe Dashboard::TodosController do
end
it 'renders 200 when user has access on given project' do
authorized_project = create(:empty_project, :public)
authorized_project = create(:project, :public)
get :index, project_id: authorized_project.id
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe DashboardController do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
before do
project.team << [user, :master]
......
require 'spec_helper'
describe Projects::BoardsController do # rubocop:disable RSpec/FilePath
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
......
require('spec_helper')
describe ProjectsController do # rubocop:disable RSpec/FilePath
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
......
......@@ -3,8 +3,8 @@ require 'spec_helper'
describe Explore::ProjectsController do
describe 'GET #trending' do
context 'sorting by update date' do
let(:project1) { create(:empty_project, :public, updated_at: 3.days.ago) }
let(:project2) { create(:empty_project, :public, updated_at: 1.day.ago) }
let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
before do
create(:trending_project, project: project1)
......
......@@ -2,8 +2,8 @@ require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
let!(:project) { create(:empty_project, group: group) }
let!(:project2) { create(:empty_project, group: group) }
let!(:project) { create(:project, group: group) }
let!(:project2) { create(:project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
let(:milestone) do
......
......@@ -3,7 +3,7 @@ require 'rails_helper'
describe GroupsController do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:empty_project, namespace: group) }
let(:project) { create(:project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
describe 'GET #index' do
......
......@@ -52,7 +52,7 @@ describe Import::BitbucketController do
end
it "assigns variables" do
@project = create(:empty_project, import_type: 'bitbucket', creator_id: user.id)
@project = create(:project, import_type: 'bitbucket', creator_id: user.id)
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
......@@ -63,7 +63,7 @@ describe Import::BitbucketController do
end
it "does not show already added project" do
@project = create(:empty_project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
@project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
......
......@@ -16,7 +16,7 @@ describe Import::FogbugzController do
end
it 'assigns variables' do
@project = create(:empty_project, import_type: 'fogbugz', creator_id: user.id)
@project = create(:project, import_type: 'fogbugz', creator_id: user.id)
stub_client(repos: [@repo])
get :status
......@@ -26,7 +26,7 @@ describe Import::FogbugzController do
end
it 'does not show already added project' do
@project = create(:empty_project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
@project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
stub_client(repos: [@repo])
get :status
......
......@@ -36,7 +36,7 @@ describe Import::GitlabController do
end
it "assigns variables" do
@project = create(:empty_project, import_type: 'gitlab', creator_id: user.id)
@project = create(:project, import_type: 'gitlab', creator_id: user.id)
stub_client(projects: [@repo])
get :status
......@@ -46,7 +46,7 @@ describe Import::GitlabController do
end
it "does not show already added project" do
@project = create(:empty_project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim')
@project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim')
stub_client(projects: [@repo])
get :status
......
......@@ -27,7 +27,7 @@ describe Import::GoogleCodeController do
end
it "assigns variables" do
@project = create(:empty_project, import_type: 'google_code', creator_id: user.id)
@project = create(:project, import_type: 'google_code', creator_id: user.id)
stub_client(repos: [@repo], incompatible_repos: [])
get :status
......@@ -38,7 +38,7 @@ describe Import::GoogleCodeController do
end
it "does not show already added project" do
@project = create(:empty_project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
@project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
stub_client(repos: [@repo], incompatible_repos: [])
get :status
......
require 'spec_helper'
describe NotificationSettingsController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:group) { create(:group, :internal) }
let(:user) { create(:user) }
......@@ -99,7 +99,7 @@ describe NotificationSettingsController do
end
context 'not authorized' do
let(:private_project) { create(:empty_project, :private) }
let(:private_project) { create(:project, :private) }
before do
sign_in(user)
......
......@@ -3,7 +3,7 @@ require 'rails_helper'
describe Projects::ApproverGroupsController do
describe '#destroy' do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
before do
......
......@@ -3,7 +3,7 @@ require 'rails_helper'
describe Projects::ApproversController do
describe '#destroy' do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
before do
......
......@@ -65,26 +65,61 @@ describe Projects::ArtifactsController do
end
describe 'GET raw' do
subject { get(:raw, namespace_id: project.namespace, project_id: project, job_id: job, path: path) }
context 'when the file exists' do
it 'serves the file using workhorse' do
get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
let(:path) { 'ci_artifacts.txt' }
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline, artifacts_file_store: store, artifacts_metadata_store: store) }
shared_examples 'a valid file' do
it 'serves the file using workhorse' do
subject
send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
expect(send_data).to start_with('artifacts-entry:')
expect(params.keys).to eq(%w(Archive Entry))
expect(params['Archive']).to start_with(archive_path)
# On object storage, the URL can end with a query string
expect(params['Archive']).to match(/build_artifacts.zip(\?[^?]+)?$/)
expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
end
expect(send_data).to start_with('artifacts-entry:')
def send_data
response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
end
base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
params = JSON.parse(Base64.urlsafe_decode64(base64_params))
def params
@params ||= begin
base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
JSON.parse(Base64.urlsafe_decode64(base64_params))
end
end
end
expect(params.keys).to eq(%w(Archive Entry))
expect(params['Archive']).to end_with('build_artifacts.zip')
expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
context 'when using local file storage' do
it_behaves_like 'a valid file' do
let(:store) { ObjectStoreUploader::LOCAL_STORE }
let(:archive_path) { ArtifactUploader.local_artifacts_store }
end
end
context 'when using remote file storage' do
before do
stub_artifacts_object_storage
end
it_behaves_like 'a valid file' do
let(:store) { ObjectStoreUploader::REMOTE_STORE }
let(:archive_path) { 'https://' }
end
end
end
context 'when the file does not exist' do
let(:path) { 'unknown' }
it 'responds Not Found' do
get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
subject
expect(response).to be_not_found
end
......
require 'spec_helper'
describe Projects::AvatarsController do
let(:project) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:user) { create(:user) }
before do
......
......@@ -193,7 +193,7 @@ describe Projects::BlobController do
context "when user doesn't have access" do
before do
other_project = create(:empty_project)
other_project = create(:project, :repository)
merge_request.update!(source_project: other_project, target_project: other_project)
end
......
require 'spec_helper'
describe Projects::Boards::IssuesController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
......
require 'spec_helper'
describe Projects::Boards::ListsController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
......
require 'spec_helper'
describe Projects::BoardsController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
......
......@@ -96,7 +96,7 @@ describe Projects::BranchesController do
end
context 'repository-less project' do
let(:project) { create :empty_project }
let(:project) { create :project }
it 'redirects to newly created branch' do
result = { status: :success, branch: double(name: branch) }
......
......@@ -24,8 +24,8 @@ describe Projects::DeployKeysController do
end
context 'when json requested' do
let(:project2) { create(:empty_project, :internal)}
let(:project_private) { create(:empty_project, :private)}
let(:project2) { create(:project, :internal)}
let(:project_private) { create(:project, :private)}
let(:deploy_key_internal) do
create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
......
......@@ -4,7 +4,7 @@ describe Projects::DeploymentsController do
include ApiHelpers
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:environment) { create(:environment, name: 'production', project: project) }
before do
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::EnvironmentsController do
set(:user) { create(:user) }
set(:project) { create(:empty_project) }
set(:project) { create(:project) }
set(:environment) do
create(:environment, name: 'production', project: project)
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::GroupLinksController do
let(:group) { create(:group, :private) }
let(:group2) { create(:group, :private) }
let(:project) { create(:empty_project, :private, group: group2) }
let(:project) { create(:project, :private, group: group2) }
let(:user) { create(:user) }
before do
......
require 'spec_helper'
describe Projects::HooksController do
let(:project) { create(:empty_project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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