Merge pull request #3133 from aignatov-bio/ai-sci-5457-update-experiments-view-screen

Connect front-end with back-end for experiments page [SCI-5457]
This commit is contained in:
aignatov-bio 2021-02-04 11:00:17 +01:00 committed by GitHub
commit b4a6644f13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 342 additions and 79 deletions

View file

@ -1,5 +1,11 @@
/* global filterDropdown */
/* global filterDropdown Sidebar Turbolinks HelperModule */
(function() {
const PERMISSIONS = ['editable', 'archivable', 'restorable', 'moveable'];
var cardsWrapper = '#cardsWrapper';
var experimentsPage = '#projectShowWrapper';
var selectedExperiments = [];
let experimentsCurrentSort;
let experimentsViewSearch;
let startedOnFromFilter;
@ -9,6 +15,77 @@
let archivedOnFromFilter;
let archivedOnToFilter;
function checkActionPermission(permission) {
return selectedExperiments.every(function(experimentId) {
return $(`.experiment-card[data-id="${experimentId}"]`).data(permission);
});
}
function updateExperimentsToolbar() {
let experimentsToolbar = $('#projectShowToolbar');
if (selectedExperiments.length === 0) {
experimentsToolbar.find('.single-object-action, .multiple-object-action').addClass('hidden');
return;
}
if (selectedExperiments.length === 1) {
experimentsToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden');
} else {
experimentsToolbar.find('.single-object-action').addClass('hidden');
experimentsToolbar.find('.multiple-object-action').removeClass('hidden');
}
PERMISSIONS.forEach((permission) => {
if (!checkActionPermission(permission)) {
experimentsToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden');
}
});
}
function initProjectsViewModeSwitch() {
$(experimentsPage).on('click', '.archive-switch', function() {
Turbolinks.visit($(this).data('url'));
});
}
function loadCardsView() {
var viewContainer = $(cardsWrapper);
$.ajax({
url: viewContainer.data('experiments-cards-url'),
type: 'GET',
dataType: 'json',
data: {
view_mode: $(experimentsPage).data('view-mode'),
sort: experimentsCurrentSort,
search: experimentsViewSearch,
created_on_from: startedOnFromFilter,
created_on_to: startedOnToFilter,
updated_on_from: modifiedOnFromFilter,
updated_on_to: modifiedOnToFilter,
archived_on_from: archivedOnFromFilter,
archived_on_to: archivedOnToFilter
},
success: function(data) {
viewContainer.find('.card').remove();
viewContainer.append(data.cards_html);
selectedExperiments.length = 0;
updateExperimentsToolbar();
},
error: function() {
viewContainer.html('Error loading project list');
}
});
}
function refreshCurrentView() {
loadCardsView();
Sidebar.reload({
sort: experimentsCurrentSort,
view_mode: $(experimentsPage).data('view-mode')
});
}
function initExperimentsFilters() {
var $filterDropdown = filterDropdown.init();
@ -41,7 +118,7 @@
archivedOnToFilter = $archivedOnToFilter.val();
experimentsViewSearch = $textFilter.val();
appliedFiltersMark();
//refreshCurrentView();
refreshCurrentView();
});
// Clear filters
@ -56,6 +133,61 @@
});
}
function initSorting() {
$('#sortMenuDropdown a').click(function() {
if (experimentsCurrentSort !== $(this).data('sort')) {
$('#sortMenuDropdown a').removeClass('selected');
experimentsCurrentSort = $(this).data('sort');
refreshCurrentView();
$(this).addClass('selected');
$('#sortMenu').dropdown('toggle');
}
});
}
function initExperimentsSelector() {
$(cardsWrapper).on('click', '.experiment-card-selector', function() {
let card = $(this).closest('.experiment-card');
let experimentId = card.data('id');
// Determine whether ID is in the list of selected project IDs
let index = $.inArray(experimentId, selectedExperiments);
// If checkbox is checked and row ID is not in list of selected project IDs
if (this.checked && index === -1) {
$(this).closest('.experiment-card').addClass('selected');
selectedExperiments.push(experimentId);
// Otherwise, if checkbox is not checked and ID is in list of selected IDs
} else if (!this.checked && index !== -1) {
$(this).closest('.experiment-card').removeClass('selected');
selectedExperiments.splice(index, 1);
}
updateExperimentsToolbar();
});
}
function initArchiveRestoreToolbarButtons() {
$(experimentsPage)
.on('ajax:before', '.archive-experiments-form, .restore-experiments-form', function() {
let buttonForm = $(this);
buttonForm.find('input[name="experiments_ids[]"]').remove();
selectedExperiments.forEach(function(id) {
$('<input>').attr({
type: 'hidden',
name: 'experiments_ids[]',
value: id
}).appendTo(buttonForm);
});
})
.on('ajax:success', '.archive-experiments-form, .restore-experiments-form', function(ev, data) {
HelperModule.flashAlertMsg(data.message, 'success');
// Project saved, reload view
refreshCurrentView();
})
.on('ajax:error', '.archive-experiments-form, .restore-experiments-form', function(ev, data) {
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
});
}
function init() {
$('.workflowimg-container').each(function() {
let container = $(this);
@ -77,6 +209,11 @@
});
initExperimentsFilters();
initSorting();
loadCardsView();
initProjectsViewModeSwitch();
initExperimentsSelector();
initArchiveRestoreToolbarButtons();
}
init();

View file

@ -7,14 +7,38 @@
// New experiments page
.projects-show {
&.active {
[data-view-mode="archived"] {
display: none !important;
}
}
&.archived {
[data-view-mode="active"] {
display: none !important;
}
}
.content-header {
.project-name {
align-items: center;
display: flex;
max-width: calc(100% - 7em);
.inline-editing-container {
width: calc(100% - 3em);
}
.fas {
margin-right: .5em;
}
}
.archive-experiments-form,
.restore-experiments-form {
display: inline-block;
}
}
.project-show-container {

View file

@ -7,9 +7,9 @@ class ExperimentsController < ApplicationController
include ApplicationHelper
include Rails.application.routes.url_helpers
before_action :load_project, only: %i(new create)
before_action :load_experiment, except: %i(new create)
before_action :check_view_permissions, except: %i(edit archive clone move new create)
before_action :load_project, only: %i(new create archive_group restore_group)
before_action :load_experiment, except: %i(new create archive_group restore_group)
before_action :check_view_permissions, except: %i(edit archive clone move new create archive_group restore_group)
before_action :check_create_permissions, only: %i(new create)
before_action :check_manage_permissions, only: %i(edit)
before_action :check_archive_permissions, only: :archive
@ -39,7 +39,7 @@ class ExperimentsController < ApplicationController
@experiment.project = @project
if @experiment.save
experiment_annotation_notification
log_activity(:create_experiment)
log_activity(:create_experiment, @experiment)
flash[:success] = t('experiments.create.success_flash',
experiment: @experiment.name)
respond_to do |format|
@ -98,7 +98,7 @@ class ExperimentsController < ApplicationController
else
:edit_experiment
end
log_activity(activity_type)
log_activity(activity_type, @experiment)
respond_to do |format|
format.json do
@ -106,7 +106,7 @@ class ExperimentsController < ApplicationController
end
format.html do
flash[:success] = t('experiments.update.success_flash',
experiment: @experiment.name)
experiment: @experiment.name)
redirect_to project_path(@experiment.project)
end
end
@ -128,7 +128,7 @@ class ExperimentsController < ApplicationController
@experiment.archived_by = current_user
@experiment.archived_on = DateTime.now
if @experiment.save
log_activity(:archive_experiment)
log_activity(:archive_experiment, @experiment)
flash[:success] = t('experiments.archive.success_flash',
experiment: @experiment.name)
@ -139,6 +139,51 @@ class ExperimentsController < ApplicationController
end
end
def archive_group
experiments = @project.experiments.active.where(id: params[:experiments_ids])
counter = 0
experiments.each do |experiment|
next unless can_archive_experiment?(experiment)
experiment.transaction do
experiment.archived_on = DateTime.now
experiment.archive!(current_user)
log_activity(:archive_experiment, experiment)
counter += 1
rescue StandardError => e
Rails.logger.error e.message
raise ActiveRecord::Rollback
end
end
if counter.positive?
render json: { message: t('experiments.archive_group.success_flash', number: counter) }
else
render json: { message: t('experiments.archive_group.error_flash') }, status: :unprocessable_entity
end
end
def restore_group
experiments = @project.experiments.archived.where(id: params[:experiments_ids])
counter = 0
experiments.each do |experiment|
next unless can_restore_experiment?(experiment)
experiment.transaction do
experiment.restore!(current_user)
log_activity(:restore_experiment, experiment)
counter += 1
rescue StandardError => e
Rails.logger.error e.message
raise ActiveRecord::Rollback
end
end
if counter.positive?
render json: { message: t('experiments.restore_group.success_flash', number: counter) }
else
render json: { message: t('experiments.restore_group.error_flash') }, status: :unprocessable_entity
end
end
# GET: clone_modal_experiment_path(id)
def clone_modal
@projects = @experiment.projects_with_role_above_user(current_user)
@ -287,6 +332,7 @@ class ExperimentsController < ApplicationController
def set_inline_name_editing
return unless can_manage_experiment?(@experiment)
@inline_editable_title_config = {
name: 'title',
params_group: 'experiment',
@ -311,13 +357,13 @@ class ExperimentsController < ApplicationController
)
end
def log_activity(type_of)
def log_activity(type_of, experiment)
Activities::CreateActivityService
.call(activity_type: type_of,
owner: current_user,
team: @experiment.project.team,
project: @experiment.project,
subject: @experiment,
message_items: { experiment: @experiment.id })
team: experiment.project.team,
project: experiment.project,
subject: experiment,
message_items: { experiment: experiment.id })
end
end

View file

@ -5,14 +5,15 @@ class ProjectsController < ApplicationController
include TeamsHelper
include InputSanitizeHelper
include ProjectsHelper
include ExperimentsHelper
attr_reader :current_folder
helper_method :current_folder
before_action :switch_team_with_param, only: :index
before_action :load_vars, only: %i(show edit update notifications experiment_archive sidebar)
before_action :load_vars, only: %i(show edit update notifications experiment_archive sidebar experiments_cards)
before_action :load_current_folder, only: %i(index cards new show experiment_archive)
before_action :check_view_permissions, only: %i(show notifications experiment_archive sidebar)
before_action :check_view_permissions, only: %i(show notifications experiment_archive sidebar experiments_cards)
before_action :check_create_permissions, only: %i(new create)
before_action :check_manage_permissions, only: :edit
before_action :set_inline_name_editing, only: %i(show)
@ -53,15 +54,15 @@ class ProjectsController < ApplicationController
end
def sidebar
respond_to do |format|
format.json do
render json: {
html: render_to_string(
partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project }
)
@current_sort = @project.current_view_state(current_user).state.dig('experiments', params[:view_mode], 'sort')
render json: {
html: render_to_string(
partial: 'shared/sidebar/experiments.html.erb', locals: {
project: @project,
view_mode: experiments_view_mode(@project)
}
end
end
)
}
end
def new
@ -258,6 +259,17 @@ class ProjectsController < ApplicationController
current_team_switch(@project.team)
end
def experiments_cards
overview_service = ExperimentsOverviewService.new(@project, current_user, params)
render json: {
cards_html: render_to_string(
partial: 'projects/show/experiments_list.html.erb',
locals: { cards: overview_service.experiments }
)
}
end
def notifications
@modules = @project
.assigned_modules(current_user)

View file

@ -1,6 +1,12 @@
# frozen_string_literal: true
module ExperimentsHelper
def experiments_view_mode(project)
return 'archived' if project.archived?
params[:view_mode] == 'archived' ? 'archived' : 'active'
end
def grouped_by_prj(experiments)
ungrouped_experiments = experiments.joins(:project)
.select('projects.name as project_name,

View file

@ -46,6 +46,11 @@ class ExperimentsOverviewService
records = records.where('experiments.created_at > ?', @params[:created_on_from])
end
records = records.where('experiments.created_at < ?', @params[:created_on_to]) if @params[:created_on_to].present?
if @params[:updated_on_from].present?
records = records.where('experiments.updated_at > ?', @params[:updated_on_from])
end
records = records.where('experiments.updated_at < ?', @params[:updated_on_to]) if @params[:updated_on_to].present?
if @params[:archived_on_from].present?
records = records.where('COALESCE(experiments.archived_on, projects.archived_on) > ?', @params[:archived_on_from])
end

View file

@ -48,27 +48,26 @@
<%= t("projects.index.filters_modal.folders.popover_html") %>
</div>
</div>
</div>
<% end %>
<div class="dropdown sort-menu">
<button class="btn btn-light dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span><i class="fas fa-sort-amount-down"></i></span>
<span class="caret"></span>
</button>
<ul id="sortMenuDropdown" class="dropdown-menu sort-projects-menu dropdown-menu-right" aria-labelledby="sortMenu">
<% %w(new old atoz ztoa archived_new archived_old).each_with_index do |sort, i| %>
<% if i.even? && i.positive? %>
<li class="divider" <%= i > 3 ? 'data-view-mode=archived' : '' %>></li>
<% end %>
<div class="dropdown sort-menu">
<button class="btn btn-light dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span><i class="fas fa-sort-amount-down"></i></span>
<span class="caret"></span>
</button>
<ul id="sortMenuDropdown" class="dropdown-menu sort-projects-menu dropdown-menu-right" aria-labelledby="sortMenu">
<% %w(new old atoz ztoa archived_new archived_old).each_with_index do |sort, i| %>
<% if i.even? && i.positive? %>
<li class="divider" <%= i > 3 ? 'data-view-mode=archived' : '' %>></li>
<% end %>
<li <%= %w(archived_new archived_old).include?(sort) ? 'data-view-mode=archived' : '' %>>
<a class="<%= 'selected' if @current_sort == sort %>"
data-sort="<%= sort %>" >
<%= t("general.sort.#{sort}_html") %>
</a>
</li>
<% end %>
<li <%= %w(archived_new archived_old).include?(sort) ? 'data-view-mode=archived' : '' %>>
<a class="<%= 'selected' if @current_sort == sort %>"
data-sort="<%= sort %>" >
<%= t("general.sort.#{sort}_html") %>
</a>
</li>
<% end %>
</ul>
</ul>
</div>
</div>
</div>
</div>

View file

@ -1,21 +1,21 @@
<% provide(:head_title, t("projects.show.head_title", project: h(@project.name)).html_safe) %>
<% provide(:sidebar_title, t("sidebar.experiments.sidebar_title")) %>
<% provide(:sidebar_url, project_sidebar_path(@project)) %>
<% provide(:sidebar_url, sidebar_project_path(@project)) %>
<% provide(:container_class, 'no-second-nav-container') %>
<%= content_for :sidebar do %>
<%= render partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project, archived: false } %>
<%= render partial: 'shared/sidebar/experiments.html.erb', locals: { project: @project, view_mode: experiments_view_mode(@project)} %>
<% end %>
<% content_for :breadcrumbs do %>
<%= render partial: 'projects/index/breadcrumbs', locals: { target_folder: @project.project_folder } %>
<% end %>
<div id="projectShowWrapper" class="content-pane flexible projects-show">
<div id="projectShowWrapper" class="content-pane flexible projects-show <%= experiments_view_mode(@project) %>" data-view-mode="<%= experiments_view_mode(@project) %>">
<%= render partial: 'projects/show/header' %>
<div class="project-show-container">
<div class="cards-wrapper" id="cardsWrapper">
<div class="cards-wrapper" id="cardsWrapper" data-experiments-cards-url="<%= experiments_cards_project_path(@project) %>">
<!-- list -->
<div class="table-header">
<div class="table-header-cell select-all-checkboxes">
@ -31,11 +31,6 @@
<div class="table-header-cell"><%= t('experiments.card.description') %></div>
<div class="table-header-cell"></div>
</div>
<!-- cards -->
<% @project.sorted_experiments(@current_sort).each do |experiment| %>
<%= render partial: 'projects/show/experiment_card',
locals: { experiment: experiment } %>
<% end %>
</div>
</div>
</div>

View file

@ -1,4 +1,11 @@
<div class="card experiment-card">
<div class="card experiment-card"
data-id="<%= experiment.id %>"
data-edit-url=""
data-editable="<%= can_manage_experiment?(experiment) %>"
data-moveable="<%= can_move_experiment?(experiment) %>"
data-archivable="<%= experiment.active? && can_archive_experiment?(experiment) %>"
data-restorable="<%= experiment.archived? && can_restore_experiment?(experiment) %>">
>
<div class="checkbox-cell table-cell">
<div class="sci-checkbox-container">
<input value="1" type="checkbox" class="sci-checkbox experiment-card-selector">

View file

@ -0,0 +1,5 @@
<% cards.each do |card| %>
<% cache [current_user, card] do %>
<%= render partial: 'projects/show/experiment_card', locals: { experiment: card } %>
<% end %>
<% end %>

View file

@ -1,6 +1,7 @@
<div class="content-header sticky-header">
<div class="title-row">
<h1 class="project-name">
<i class="fas fa-archive" data-view-mode="archived"></i>
<% if @inline_editable_title_config.present? %>
<%= render partial: "shared/inline_editing",
locals: {
@ -8,9 +9,6 @@
config: @inline_editable_title_config
} %>
<% else %>
<% if action_name == 'experiment_archive' %>
<i class="fas fa-archive"></i>
<% end %>
<%= @project.name %>
<% end %>
</h1>
@ -50,18 +48,19 @@
<button class="btn btn-light icon-btn dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="fas fa-sort-amount-up"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="sortMenu">
<% ["new", "old", "atoz", "ztoa", "archived_new", "archived_old"].each_with_index do |sort, i| %>
<% unless action_name != 'experiment_archive' && sort.include?('arch') %>
<ul id="sortMenuDropdown" class="dropdown-menu sort-experiments-menu dropdown-menu-right" aria-labelledby="sortMenu">
<% %w(new old atoz ztoa archived_new archived_old).each_with_index do |sort, i| %>
<% if i.even? && i.positive? %>
<li class="divider"></li>
<li class="divider" <%= i > 3 ? 'data-view-mode=archived' : '' %>></li>
<% end %>
<li>
<a class="<%= 'selected' if @current_sort == sort %>" href="?<%= {sort: sort}.reject{|v| v.to_s == "0"}.to_query %>"><%= t("general.sort.#{sort}_html") %></a>
<li <%= %w(archived_new archived_old).include?(sort) ? 'data-view-mode=archived' : '' %>>
<a class="<%= 'selected' if @current_sort == sort %>"
data-sort="<%= sort %>" >
<%= t("general.sort.#{sort}_html") %>
</a>
</li>
<% end %>
<% end %>
</ul>
</ul>
</div>
</div>
</div>

View file

@ -4,6 +4,7 @@
<%= link_to new_project_experiment_url(@project),
remote: true,
type: "button",
data: {view_mode: :active},
id: 'new-experiment',
class: 'btn btn-primary' do %>
<span class="fas fa-plus" aria-hidden="true"></span>
@ -11,23 +12,38 @@
<% end %>
<% end %>
<a href="#" class="btn btn-light edit-experiment-btn single-object-action" data-for="editable" data-url="">
<a href="#" class="btn btn-light edit-experiment-btn single-object-action hidden" data-for="editable" data-view-mode="active" data-url="">
<span class="fas fa-pencil-alt" aria-hidden="true"></span>
<span class="hidden-xs"><%= t('experiments.toolbar.edit_button') %></span>
</a>
<a href="#" class="btn btn-light duplicate-experiment-btn multiple-object-action" data-for="duplicable" data-url="">
<a href="#" class="btn btn-light duplicate-experiment-btn multiple-object-action hidden" data-view-mode="active" data-for="duplicable" data-url="">
<span class="fas fa-copy" aria-hidden="true"></span>
<span class="hidden-xs"><%= t('experiments.toolbar.duplicate_button') %></span>
</a>
<a href="#" class="btn btn-light move-experiments-btn multiple-object-action" data-for="moveable" data-url="">
<a href="#" class="btn btn-light move-experiments-btn multiple-object-action hidden" data-view-mode="active" data-for="moveable" data-url="">
<span class="fas fa-arrow-right" aria-hidden="true"></span>
<span class="hidden-xs"><%= t('experiments.toolbar.move_button') %></span>
</a>
<a href="#" class="btn btn-light archive-experiments-btn multiple-object-action" data-for="archivable" data-url="">
<%= button_to archive_group_project_experiments_path(@project),
class: 'btn btn-light archive-experiments-btn multiple-object-action hidden',
form_class: 'archive-experiments-form',
data: { for: :archivable, view_mode: 'active' },
remote: true,
method: :post do %>
<span class="fas fa-archive" aria-hidden="true"></span>
<span class="hidden-xs"><%= t('experiments.toolbar.archive_button') %></span>
</a>
<span class="hidden-xs"><%= t('projects.index.archive_button') %></span>
<% end %>
<%= button_to restore_group_project_experiments_path(@project),
class: 'btn btn-light restore-experiments-btn multiple-object-action hidden',
form_class: 'restore-experiments-form',
data: { for: :restorable, view_mode: 'archived' },
remote: true,
method: :post do %>
<span class="fas fa-undo" aria-hidden="true"></span>
<span class="hidden-xs"><%= t('experiments.toolbar.restore_button') %></span>
<% end %>
</div>

View file

@ -2,24 +2,24 @@
<li class="sidebar-leaf">
<% if @project.archived? %>
<%= link_to t('sidebar.experiments.back_archived_projects'), projects_path(view_mode: :archived), class: 'sidebar-link back-button' %>
<% elsif archived %>
<% elsif view_mode == 'archived' %>
<%= link_to t('sidebar.experiments.back_active_experiments'), project_url(@project), class: 'sidebar-link back-button' %>
<% else %>
<%= link_to t('sidebar.experiments.back_button'), projects_path, class: 'sidebar-link back-button' %>
<% end %>
</li>
<% project.sorted_experiments(@current_sort, archived).each do |experiment| %>
<% project.sorted_experiments(@current_sort, view_mode == 'archived').each do |experiment| %>
<li class="sidebar-leaf">
<% if archived %>
<% if view_mode == 'archived' %>
<%= link_to experiment.name, module_archive_experiment_path(experiment), class: 'sidebar-link' %>
<% else %>
<%= link_to experiment.name, canvas_experiment_path(experiment), class: 'sidebar-link' %>
<% end %>
</li>
<% end %>
<% unless archived %>
<% if view_mode == 'active' %>
<li class="sidebar-leaf">
<%= link_to experiment_archive_project_url(@project), class: 'sidebar-link' do %>
<%= link_to project_path(@project, view_mode: 'archived'), class: 'sidebar-link' do %>
<i class="fas fa-archive"></i>
<%= t('sidebar.experiments.archived_experiments') %>
<% end %>

View file

@ -954,6 +954,7 @@ en:
duplicate_button: "Duplicate"
move_button: "Move"
archive_button: "Archive"
restore_button: "Restore"
card:
name: "Experiment"
start_date: "Start date"
@ -989,6 +990,12 @@ en:
success_flash: "Successfully archived experiment %{experiment}"
error_flash: 'Could not archive the experiment.'
label_title: 'Archive'
archive_group:
success_flash: "<strong>%{number}</strong> experiment(s) successfully archived."
error_flash: 'Could not archive experiments.'
restore_group:
success_flash: "<strong>%{number}</strong> experiment(s) successfully restored."
error_flash: "Failed to restore experiment(s)."
clone:
modal_title: 'Copy experiment %{experiment} as template'
label_title: 'Copy as template'

View file

@ -280,19 +280,24 @@ Rails.application.routes.draw do
as: :save_modal
end
end
resources :experiments, only: %i(new create), defaults: { format: 'json' }
resources :experiments, only: %i(new create), defaults: { format: 'json' } do
collection do
post 'archive_group' # archive group of experements
post 'restore_group' # restore group of experements
end
end
member do
# Notifications popup for individual project in projects index
get 'notifications'
get 'experiment_archive' # Experiment archive for single project
get 'experiments_cards'
get 'sidebar'
end
# This route is defined outside of member block
# to preserve original :project_id parameter in URL.
get 'users/edit', to: 'user_projects#index_edit'
get 'sidebar', to: 'projects#sidebar', as: 'sidebar'
collection do
get 'cards', to: 'projects#cards'
get 'users_filter'