Commit 5bb6a85b authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Refactor projects filtering by name

Reuse same search form and behavior for dashboard#projects, group#projects
and admin#projects. Repsect all other options like sorting, personal
filter when search projects by name. Create FilterableList JS class to
handle identical behaviour of projects and groups lists.

This change also makes filtering and sorting availabe on explore#projects
and explore#groups no matter if you are logged in or not.
Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parent 8fd5aeee
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
/* global Shortcuts */ /* global Shortcuts */
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout'); const UserCallout = require('./user_callout');
...@@ -98,6 +99,14 @@ const UserCallout = require('./user_callout'); ...@@ -98,6 +99,14 @@ const UserCallout = require('./user_callout');
case 'dashboard:todos:index': case 'dashboard:todos:index':
new gl.Todos(); new gl.Todos();
break; break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
case 'explore:projects:index':
case 'explore:projects:trending':
case 'explore:projects:starred':
case 'admin:projects:index':
new ProjectsList();
break;
case 'dashboard:groups:index': case 'dashboard:groups:index':
case 'explore:groups:index': case 'explore:groups:index':
new GroupsList(); new GroupsList();
...@@ -163,9 +172,6 @@ const UserCallout = require('./user_callout'); ...@@ -163,9 +172,6 @@ const UserCallout = require('./user_callout');
case 'dashboard:activity': case 'dashboard:activity':
new gl.Activities(); new gl.Activities();
break; break;
case 'dashboard:projects:starred':
new gl.Activities();
break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit(); new Commit();
new gl.Diff(); new gl.Diff();
...@@ -208,6 +214,7 @@ const UserCallout = require('./user_callout'); ...@@ -208,6 +214,7 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new NotificationsForm(); new NotificationsForm();
new NotificationsDropdown(); new NotificationsDropdown();
new ProjectsList();
break; break;
case 'groups:group_members:index': case 'groups:group_members:index':
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
......
/**
* Makes search request for content when user types a value in the search input.
* Updates the html content of the page with the received one.
*/
export default class FilterableList {
constructor(form, filter, holder) {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
this.initSearch();
}
initSearch() {
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
this.listFilterElement.removeEventListener('input', this.debounceFilter);
this.listFilterElement.addEventListener('input', this.debounceFilter);
}
filterResults() {
const form = this.filterForm;
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: form.getAttribute('action'),
data: $(form).serialize(),
type: 'GET',
dataType: 'json',
context: this,
complete() {
$(this.listHolderElement).fadeTo(250, 1);
},
success(data) {
this.listHolderElement.innerHTML = data.html;
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: filterUrl,
}, document.title, filterUrl);
},
});
}
}
import FilterableList from './filterable_list';
/** /**
* Based on project list search.
* Makes search request for groups when user types a value in the search input. * Makes search request for groups when user types a value in the search input.
* Updates the html content of the page with the received one. * Updates the html content of the page with the received one.
*/ */
export default class GroupsList { export default class GroupsList {
constructor() { constructor() {
this.groupsListFilterElement = document.querySelector('.js-groups-list-filter'); var form = document.querySelector('form#group-filter-form');
this.groupsListHolderElement = document.querySelector('.js-groups-list-holder'); var filter = document.querySelector('.js-groups-list-filter');
var holder = document.querySelector('.js-groups-list-holder');
this.initSearch();
}
initSearch() {
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
this.groupsListFilterElement.removeEventListener('input', this.debounceFilter);
this.groupsListFilterElement.addEventListener('input', this.debounceFilter);
}
filterResults() {
const form = document.querySelector('form#group-filter-form');
const groupFilterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
$(this.groupsListHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: form.getAttribute('action'),
data: $(form).serialize(),
type: 'GET',
dataType: 'json',
context: this,
complete() {
$(this.groupsListHolderElement).fadeTo(250, 1);
},
success(data) {
this.groupsListHolderElement.innerHTML = data.html;
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: groupFilterUrl,
}, document.title, groupFilterUrl); new FilterableList(form, filter, holder);
},
});
} }
} }
/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */ import FilterableList from './filterable_list';
(function() { /**
window.ProjectsList = { * Makes search request for projects when user types a value in the search input.
init: function() { * Updates the html content of the page with the received one.
$(".projects-list-filter").off('keyup'); */
this.initSearch(); export default class ProjectsList {
return this.initPagination(); constructor() {
}, var form = document.querySelector('form#project-filter-form');
initSearch: function() { var filter = document.querySelector('.js-projects-list-filter');
var debounceFilter, projectsListFilter; var holder = document.querySelector('.js-projects-list-holder');
projectsListFilter = $('.projects-list-filter');
debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); new FilterableList(form, filter, holder);
return projectsListFilter.on('keyup', function(e) { }
if (projectsListFilter.val() !== '') { }
return debounceFilter();
}
});
},
filterResults: function() {
var form, project_filter_url, search;
$('.projects-list-holder').fadeTo(250, 0.5);
form = null;
form = $("form#project-filter-form");
search = $(".projects-list-filter").val();
project_filter_url = form.attr('action') + '?' + form.serialize();
return $.ajax({
type: "GET",
url: form.attr('action'),
data: form.serialize(),
complete: function() {
return $('.projects-list-holder').fadeTo(250, 1);
},
success: function(data) {
$('.projects-list-holder').replaceWith(data.html);
return history.replaceState({
page: project_filter_url
// Change url so if user reload a page - search results are saved
}, document.title, project_filter_url);
},
dataType: "json"
});
},
initPagination: function() {
return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) {
return $('.projects-list-holder').replaceWith(data.html);
});
}
};
}).call(window);
...@@ -182,7 +182,8 @@ input[type="checkbox"]:hover { ...@@ -182,7 +182,8 @@ input[type="checkbox"]:hover {
display: flex; display: flex;
} }
.search-field-holder { .search-field-holder,
.project-filter-form {
-webkit-flex: 1 0 auto; -webkit-flex: 1 0 auto;
flex: 1 0 auto; flex: 1 0 auto;
position: relative; position: relative;
...@@ -201,7 +202,8 @@ input[type="checkbox"]:hover { ...@@ -201,7 +202,8 @@ input[type="checkbox"]:hover {
pointer-events: none; pointer-events: none;
} }
.search-text-input { .search-text-input,
.project-filter-form-field {
padding-left: $gl-padding + 15px; padding-left: $gl-padding + 15px;
padding-right: $gl-padding + 15px; padding-right: $gl-padding + 15px;
} }
......
...@@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects })
}
end
end
end end
def show def show
......
module ExploreHelper module ExploreHelper
def filter_projects_path(options = {}) def filter_projects_path(options = {})
exist_opts = { exist_opts = {
sort: params[:sort], sort: params[:sort] || @sort,
scope: params[:scope], scope: params[:scope],
group: params[:group], group: params[:group],
tag: params[:tag], tag: params[:tag],
visibility_level: params[:visibility_level], visibility_level: params[:visibility_level],
name: params[:name],
personal: params[:personal],
archived: params[:archived],
shared: params[:shared],
namespace_id: params[:namespace_id],
} }
options = exist_opts.merge(options) options = exist_opts.merge(options)
......
.js-projects-list-holder
- if @projects.any?
%ul.projects-list.content-list
- @projects.each_with_index do |project|
%li.project-row
.controls
- if project.archived
%span.label.label-warning archived
%span.badge
= storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
.avatar-container.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
%span.project-name.filter-title
= project.name
- if project.description.present?
.description
= markdown_field(project, :description)
= paginate @projects, theme: 'gitlab'
- else
.nothing-here-block No projects found
...@@ -7,33 +7,24 @@ ...@@ -7,33 +7,24 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
.prepend-top-default .prepend-top-default
= form_tag admin_projects_path, method: :get do |f| .search-holder
= render "shared/projects/filter_fields" = render 'shared/projects/search_form', autofocus: true, icon: true
.search-holder .dropdown
.search-field-holder - toggle_text = 'Namespace'
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' - if params[:namespace_id].present?
= hidden_field_tag :namespace_id, params[:namespace_id]
- if params[:visibility_level].present? - namespace = Namespace.find(params[:namespace_id])
= hidden_field_tag 'visibility_level', params[:visibility_level] - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
= icon("search", class: "search-icon") .dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
.dropdown = dropdown_filter("Search for Namespace")
- toggle_text = 'Namespace' = dropdown_content
- if params[:namespace_id].present? = dropdown_loading
= hidden_field_tag :namespace_id, params[:namespace_id] = render 'shared/projects/dropdown'
- namespace = Namespace.find(params[:namespace_id]) = link_to new_project_path, class: 'btn btn-new' do
- toggle_text = "#{namespace.kind}: #{namespace.full_path}" New Project
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) = button_tag "Search", class: "btn btn-primary btn-search hide"
.dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
= dropdown_content
= dropdown_loading
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'btn btn-new' do
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links %ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
...@@ -51,35 +42,4 @@ ...@@ -51,35 +42,4 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public Public
.projects-list-holder = render 'projects'
- if @projects.any?
%ul.projects-list.content-list
- @projects.each_with_index do |project|
%li.project-row
.controls
- if project.archived
%span.label.label-warning archived
%span.badge
= storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
.avatar-container.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
%span.project-name.filter-title
= project.name
- if project.description.present?
.description
= markdown_field(project, :description)
= paginate @projects, theme: 'gitlab'
- else
.nothing-here-block No projects found
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
= link_to explore_groups_path, title: 'Explore groups' do = link_to explore_groups_path, title: 'Explore groups' do
Explore Groups Explore Groups
.nav-controls .nav-controls
= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| = render 'shared/groups/search_form'
= search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
= render 'shared/groups/dropdown' = render 'shared/groups/dropdown'
- if current_user.can_create_group? - if current_user.can_create_group?
= link_to new_group_path, class: "btn btn-new" do = link_to new_group_path, class: "btn btn-new" do
......
...@@ -13,9 +13,7 @@ ...@@ -13,9 +13,7 @@
Explore projects Explore projects
.nav-controls .nav-controls
= form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = render 'shared/projects/search_form'
= render "shared/projects/filter_fields"
= search_field_tag :name, params[:name], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
- if current_user.can_create_project? - if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do = link_to new_project_path, class: 'btn btn-new' do
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') } .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @projects.any? || params[:name] - if @projects.any? || params[:name]
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
......
.top-area
%ul.nav-links
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path do
Explore Groups
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= render 'dashboard/groups_head' = render 'dashboard/groups_head'
- else - else
= render 'explore/head' = render 'explore/head'
= render 'nav'
- if @groups.present? - if @groups.present?
= render 'groups' = render 'groups'
......
%ul.nav-links .top-area
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do %ul.nav-links
= link_to trending_explore_projects_path do = nav_link(page: [trending_explore_projects_path, explore_root_path]) do
Trending = link_to trending_explore_projects_path do
= nav_link(page: starred_explore_projects_path) do Trending
= link_to starred_explore_projects_path do = nav_link(page: starred_explore_projects_path) do
Most stars = link_to starred_explore_projects_path do
= nav_link(page: explore_projects_path) do Most stars
= link_to explore_projects_path do = nav_link(page: explore_projects_path) do
All = link_to explore_projects_path do
All
.nav-controls
- unless current_user
= render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
= render 'filter'
...@@ -6,10 +6,5 @@ ...@@ -6,10 +6,5 @@
- else - else
= render 'explore/head' = render 'explore/head'
.top-area = render 'explore/projects/nav'
= render 'explore/projects/nav'
.nav-controls
= render 'filter'
= render 'projects', projects: @projects = render 'projects', projects: @projects
...@@ -11,9 +11,7 @@ ...@@ -11,9 +11,7 @@
.top-area .top-area
= render 'groups/show_nav' = render 'groups/show_nav'
.nav-controls .nav-controls
= form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = render 'shared/projects/search_form'
= render "shared/projects/filter_fields"
= search_field_tag :name, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group - if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
......
= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f|
= search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_recently_updated
- name = params[:name]
- personal = params[:personal]
- archived = params[:archived]
- shared = params[:shared]
- namespace_id = params[:namespace_id]
.dropdown .dropdown
- toggle_text = projects_sort_options_hash[@sort] - toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
...@@ -12,32 +7,32 @@ ...@@ -12,32 +7,32 @@
Sort by Sort by
- projects_sort_options_hash.each do |value, title| - projects_sort_options_hash.each do |value, title|
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal, name: name), class: ("is-active" if @sort == value) do = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do
= title = title
%li.divider %li.divider
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil, name: name), class: ("is-active" unless params[:archived].present?) do = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects Hide archived projects
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true, name: name), class: ("is-active" if params[:archived].present?) do = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do
Show archived projects Show archived projects
- if current_user - if current_user
%li.divider %li.divider
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil, name: name), class: ("is-active" unless personal.present?) do = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do
Owned by anyone Owned by anyone
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true, name: name), class: ("is-active" if personal.present?) do = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do
Owned by me Owned by me
- if @group && @group.shared_projects.present? - if @group && @group.shared_projects.present?
%li.divider %li.divider
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil, name: name), class: ("is-active" unless shared.present?) do = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
All projects All projects
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0, name: name), class: ("is-active" if shared == '0') do = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
Hide shared projects Hide shared projects
%li %li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1, name: name), class: ("is-active" if shared == '1') do = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
Hide group projects Hide group projects
- if params[:sort].present?
= hidden_field_tag :sort, params[:sort]
- if params[:personal].present?
= hidden_field_tag :personal, params[:personal]
- if params[:archived].present?
= hidden_field_tag :archived, params[:archived]
- if params[:visibility_level].present?
= hidden_field_tag :visibility_level, params[:visibility_level]
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true - remote = false unless local_assigns[:remote] == true
.projects-list-holder .js-projects-list-holder
- if projects.any? - if projects.any?
%ul.projects-list.content-list %ul.projects-list.content-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
...@@ -25,6 +25,3 @@ ...@@ -25,6 +25,3 @@
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else - else
.nothing-here-block No projects found .nothing-here-block No projects found
:javascript
ProjectsList.init();
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: 'Filter by name...',
class: 'project-filter-form-field form-control input-short js-projects-list-filter',
spellcheck: false,
id: 'project-filter-form-field',
tabindex: "2",
autofocus: local_assigns[:autofocus]
- if local_assigns[:icon]
= icon("search", class: "search-icon")
- if params[:sort].present?
= hidden_field_tag :sort, params[:sort]
- if params[:personal].present?
= hidden_field_tag :personal, params[:personal]
- if params[:archived].present?
= hidden_field_tag :archived, params[:archived]
- if params[:visibility_level].present?
= hidden_field_tag :visibility_level, params[:visibility_level]
...@@ -56,7 +56,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -56,7 +56,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end end
step 'I should see my fork on the list' do step 'I should see my fork on the list' do
page.within('.projects-list-holder') do page.within('.js-projects-list-holder') do
project = @user.fork_of(@project) project = @user.fork_of(@project)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment