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 @@
/* global Shortcuts */
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
......@@ -98,6 +99,14 @@ const UserCallout = require('./user_callout');
case 'dashboard:todos:index':
new gl.Todos();
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 'explore:groups:index':
new GroupsList();
......@@ -163,9 +172,6 @@ const UserCallout = require('./user_callout');
case 'dashboard:activity':
new gl.Activities();
break;
case 'dashboard:projects:starred':
new gl.Activities();
break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
......@@ -208,6 +214,7 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
new NotificationsDropdown();
new ProjectsList();
break;
case 'groups:group_members:index':
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.
* Updates the html content of the page with the received one.
*/
export default class GroupsList {
constructor() {
this.groupsListFilterElement = document.querySelector('.js-groups-list-filter');
this.groupsListHolderElement = 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,
var form = document.querySelector('form#group-filter-form');
var filter = document.querySelector('.js-groups-list-filter');
var holder = document.querySelector('.js-groups-list-holder');
}, 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 = {
init: function() {
$(".projects-list-filter").off('keyup');
this.initSearch();
return this.initPagination();
},
initSearch: function() {
var debounceFilter, projectsListFilter;
projectsListFilter = $('.projects-list-filter');
debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
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);
});
/**
* Makes search request for projects when user types a value in the search input.
* Updates the html content of the page with the received one.
*/
export default class ProjectsList {
constructor() {
var form = document.querySelector('form#project-filter-form');
var filter = document.querySelector('.js-projects-list-filter');
var holder = document.querySelector('.js-projects-list-holder');
new FilterableList(form, filter, holder);
}
};
}).call(window);
}
......@@ -182,7 +182,8 @@ input[type="checkbox"]:hover {
display: flex;
}
.search-field-holder {
.search-field-holder,
.project-filter-form {
-webkit-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
......@@ -201,7 +202,8 @@ input[type="checkbox"]:hover {
pointer-events: none;
}
.search-text-input {
.search-text-input,
.project-filter-form-field {
padding-left: $gl-padding + 15px;
padding-right: $gl-padding + 15px;
}
......
......@@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
@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
def show
......
module ExploreHelper
def filter_projects_path(options = {})
exist_opts = {
sort: params[:sort],
sort: params[:sort] || @sort,
scope: params[:scope],
group: params[:group],
tag: params[:tag],
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)
......
.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,17 +7,8 @@
%div{ class: container_class }
.top-area
.prepend-top-default
= form_tag admin_projects_path, method: :get do |f|
= render "shared/projects/filter_fields"
.search-holder
.search-field-holder
= 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[:visibility_level].present?
= hidden_field_tag 'visibility_level', params[:visibility_level]
= icon("search", class: "search-icon")
= render 'shared/projects/search_form', autofocus: true, icon: true
.dropdown
- toggle_text = 'Namespace'
- if params[:namespace_id].present?
......@@ -51,35 +42,4 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.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
= render 'projects'
......@@ -7,8 +7,7 @@
= link_to explore_groups_path, title: 'Explore groups' do
Explore Groups
.nav-controls
= 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"
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
- if current_user.can_create_group?
= link_to new_group_path, class: "btn btn-new" do
......
......@@ -13,9 +13,7 @@
Explore projects
.nav-controls
= form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= 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/search_form'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do
......
......@@ -5,7 +5,6 @@
- header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @projects.any? || params[:name]
= 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 @@
= render 'dashboard/groups_head'
- else
= render 'explore/head'
= render 'nav'
- if @groups.present?
= render 'groups'
......
%ul.nav-links
.top-area
%ul.nav-links
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do
= link_to trending_explore_projects_path do
Trending
......@@ -8,3 +9,9 @@
= nav_link(page: explore_projects_path) do
= 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 @@
- else
= render 'explore/head'
.top-area
= render 'explore/projects/nav'
.nav-controls
= render 'filter'
= render 'explore/projects/nav'
= render 'projects', projects: @projects
......@@ -11,9 +11,7 @@
.top-area
= render 'groups/show_nav'
.nav-controls
= form_tag request.fullpath, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= 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/search_form'
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
= 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
- name = params[:name]
- personal = params[:personal]
- archived = params[:archived]
- shared = params[:shared]
- namespace_id = params[:namespace_id]
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
......@@ -12,32 +7,32 @@
Sort by
- projects_sort_options_hash.each do |value, title|
%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
%li.divider
%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
%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
- if current_user
%li.divider
%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
%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
- if @group && @group.shared_projects.present?
%li.divider
%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
%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
%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
- 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 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
.projects-list-holder
.js-projects-list-holder
- if projects.any?
%ul.projects-list.content-list
- projects.each_with_index do |project, i|
......@@ -25,6 +25,3 @@
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.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
end
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)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
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