mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-01-28 10:37:52 +08:00
Implement new Projects bottom toolbar [SCI-8295]
This commit is contained in:
parent
5cf5201efd
commit
ed04bc6b6a
9 changed files with 351 additions and 91 deletions
|
@ -11,7 +11,6 @@
|
|||
/* eslint-disable no-use-before-define */
|
||||
|
||||
var ProjectsIndex = (function() {
|
||||
const PERMISSIONS = ['editable', 'archivable', 'restorable', 'moveable', 'deletable'];
|
||||
var projectsWrapper = '#projectsWrapper';
|
||||
var toolbarWrapper = '#toolbarWrapper';
|
||||
var cardsWrapper = '#cardsWrapper';
|
||||
|
@ -180,7 +179,7 @@ var ProjectsIndex = (function() {
|
|||
ev.preventDefault();
|
||||
// Load HTML to refresh users list
|
||||
$.ajax({
|
||||
url: $(exportProjectsBtn).data('export-projects-modal-url'),
|
||||
url: $(exportProjectsBtn).data('url'),
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
|
@ -276,34 +275,13 @@ var ProjectsIndex = (function() {
|
|||
}
|
||||
|
||||
function updateProjectsToolbar() {
|
||||
let projectsToolbar = $('#projectsToolbar');
|
||||
|
||||
if (selectedProjects.length === 0 && selectedProjectFolders.length === 0) {
|
||||
projectsToolbar.find('.single-object-action, .multiple-object-action').addClass('hidden');
|
||||
} else {
|
||||
if (selectedProjects.length + selectedProjectFolders.length === 1) {
|
||||
projectsToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden');
|
||||
if (selectedProjectFolders.length === 1) {
|
||||
projectsToolbar.find('.project-only-action').addClass('hidden');
|
||||
} else {
|
||||
projectsToolbar.find('.folders-only-action').addClass('hidden');
|
||||
}
|
||||
} else {
|
||||
projectsToolbar.find('.single-object-action').addClass('hidden');
|
||||
projectsToolbar.find('.multiple-object-action').removeClass('hidden');
|
||||
if (selectedProjectFolders.length > 0) {
|
||||
projectsToolbar.find('.project-only-action').addClass('hidden');
|
||||
}
|
||||
if (selectedProjects.length > 0) {
|
||||
projectsToolbar.find('.folder-only-action').addClass('hidden');
|
||||
}
|
||||
window.actionToolbarComponent.fetchActions(
|
||||
{
|
||||
project_ids: selectedProjects,
|
||||
project_folder_ids: selectedProjectFolders
|
||||
}
|
||||
PERMISSIONS.forEach((permission) => {
|
||||
if (!checkActionPermission(permission)) {
|
||||
projectsToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
window.actionToolbarComponent.setReloadCallback(refreshCurrentView);
|
||||
}
|
||||
|
||||
function refreshCurrentView() {
|
||||
|
@ -332,7 +310,7 @@ var ProjectsIndex = (function() {
|
|||
});
|
||||
}
|
||||
|
||||
$(toolbarWrapper).on('click', '.edit-btn', function(ev) {
|
||||
$(projectsWrapper).on('click', '.edit-btn', function(ev) {
|
||||
var editUrl = $(`.project-card[data-id=${selectedProjects[0]}]`).data('edit-url') ||
|
||||
$(`.folder-card[data-id=${selectedProjectFolders[0]}]`).data('edit-url');
|
||||
ev.stopPropagation();
|
||||
|
@ -725,17 +703,7 @@ var ProjectsIndex = (function() {
|
|||
}
|
||||
|
||||
updateSelectAllCheckbox();
|
||||
|
||||
if (this.checked) {
|
||||
$.get(projectCard.data('permissions-url'), function(result) {
|
||||
PERMISSIONS.forEach((permission) => {
|
||||
projectCard.data(permission, result[permission]);
|
||||
});
|
||||
updateProjectsToolbar();
|
||||
});
|
||||
} else {
|
||||
updateProjectsToolbar();
|
||||
}
|
||||
updateProjectsToolbar();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class ProjectsController < ApplicationController
|
|||
sidebar experiments_cards view_type actions_dropdown create_tag)
|
||||
before_action :load_current_folder, only: %i(index cards new show)
|
||||
before_action :check_view_permissions, except: %i(index cards new create edit update archive_group restore_group
|
||||
users_filter actions_dropdown)
|
||||
users_filter actions_dropdown actions_toolbar)
|
||||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_manage_permissions, only: :edit
|
||||
before_action :load_exp_sort_var, only: :show
|
||||
|
@ -242,7 +242,7 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
|
||||
def archive_group
|
||||
projects = current_team.projects.active.where(id: params[:projects_ids])
|
||||
projects = current_team.projects.active.where(id: params[:project_ids])
|
||||
counter = 0
|
||||
projects.each do |project|
|
||||
next unless can_archive_project?(project)
|
||||
|
@ -282,7 +282,7 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
|
||||
def restore_group
|
||||
projects = current_team.projects.archived.where(id: params[:projects_ids])
|
||||
projects = current_team.projects.archived.where(id: params[:project_ids])
|
||||
counter = 0
|
||||
projects.each do |project|
|
||||
next unless can_restore_project?(project)
|
||||
|
@ -370,6 +370,17 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions:
|
||||
Toolbars::ProjectsService.new(
|
||||
current_user,
|
||||
project_ids: params[:project_ids].split(','),
|
||||
project_folder_ids: params[:project_folder_ids].split(',')
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_params
|
||||
|
|
16
app/javascript/packs/vue/action_toolbar.js
Normal file
16
app/javascript/packs/vue/action_toolbar.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* global I18n */
|
||||
|
||||
import TurbolinksAdapter from 'vue-turbolinks';
|
||||
import Vue from 'vue/dist/vue.esm';
|
||||
import ActionToolbar from '../../vue/components/action_toolbar.vue';
|
||||
|
||||
Vue.use(TurbolinksAdapter);
|
||||
|
||||
window.addEventListener('turbolinks:load', () => {
|
||||
new Vue({
|
||||
el: '#actionToolbar',
|
||||
components: {
|
||||
ActionToolbar
|
||||
}
|
||||
});
|
||||
});
|
85
app/javascript/vue/components/action_toolbar.vue
Normal file
85
app/javascript/vue/components/action_toolbar.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div v-if="actions.length" class="sn-action-toolbar p-4 bg-sn-sleepy-grey w-full fixed bottom-0 rounded-t-md shadow-[0_-12px_24px_-12px_rgba(35,31,32,0.2)]" :style="`width: ${width}px`">
|
||||
<div class="sn-action-toolbar__actions flex">
|
||||
<div v-for="action in actions" :key="action.name" class="sn-action-toolbar__action">
|
||||
<a :class="`btn btn-light ${action.button_class}`"
|
||||
:href="action.type === 'link' ? action.path : '#'"
|
||||
:id="action.button_id"
|
||||
:data-url="action.path"
|
||||
:data-object-type="action.item_type"
|
||||
:data-object-id="action.item_id"
|
||||
@click="doAction(action)">
|
||||
<i :class="action.icon"></i>
|
||||
{{ action.label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ActionToolbar',
|
||||
props: {
|
||||
actionsUrl: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [],
|
||||
shown: false,
|
||||
multiple: false,
|
||||
params: {},
|
||||
reloadCallback: null,
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.actionToolbarComponent = this;
|
||||
window.onresize = this.setWidth;
|
||||
},
|
||||
mounted() {
|
||||
this.setWidth();
|
||||
},
|
||||
beforeDestroy() {
|
||||
delete window.actionToolbarComponent;
|
||||
},
|
||||
methods: {
|
||||
setWidth() {
|
||||
this.width = $(this.$el).parent().width();
|
||||
},
|
||||
fetchActions(params) {
|
||||
this.params = params;
|
||||
|
||||
$.get(`${this.actionsUrl}?${new URLSearchParams(this.params).toString()}`, (data) => {
|
||||
this.actions = data.actions;
|
||||
});
|
||||
},
|
||||
setReloadCallback(func) {
|
||||
this.reloadCallback = func;
|
||||
},
|
||||
doAction(action) {
|
||||
switch(action.type) {
|
||||
case 'legacy':
|
||||
// do nothing, this is handled by legacy code based on the button class
|
||||
break;
|
||||
case 'link':
|
||||
// already handled by href
|
||||
break;
|
||||
case 'request':
|
||||
$.ajax({
|
||||
type: action.request_method,
|
||||
url: action.path,
|
||||
data: this.params
|
||||
}).done((data) => {
|
||||
HelperModule.flashAlertMsg(data.message, 'success');
|
||||
}).fail((data) => {
|
||||
HelperModule.flashAlertMsg(data.message, 'danger');
|
||||
}).complete(() => {
|
||||
if (this.reloadCallback) this.reloadCallback();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
180
app/services/toolbars/projects_service.rb
Normal file
180
app/services/toolbars/projects_service.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Toolbars
|
||||
class ProjectsService
|
||||
attr_reader :current_user
|
||||
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def initialize(current_user, project_ids: [], project_folder_ids: [])
|
||||
@current_user = current_user
|
||||
@projects = current_user.current_team.projects.where(id: project_ids)
|
||||
@project_folders = current_user.current_team.project_folders.where(id: project_folder_ids)
|
||||
|
||||
@items = @projects + @project_folders
|
||||
|
||||
@single = @items.length == 1
|
||||
|
||||
@item_type = if project_ids.blank? && project_folder_ids.blank?
|
||||
:none
|
||||
elsif project_ids.present? && project_folder_ids.present?
|
||||
:any
|
||||
elsif project_folder_ids.present?
|
||||
:project_folder
|
||||
else
|
||||
:project
|
||||
end
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] if @item_type == :none
|
||||
|
||||
[
|
||||
edit_action(@items),
|
||||
move_action(@items),
|
||||
export_action(@items),
|
||||
archive_action(@items),
|
||||
restore_action(@items),
|
||||
comments_action(@items),
|
||||
activities_action(@items)
|
||||
].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def edit_action(items)
|
||||
return unless @single
|
||||
|
||||
return unless @item_type == :project
|
||||
|
||||
project = items.first
|
||||
|
||||
return unless can_manage_project?(project)
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
label: I18n.t('projects.index.edit_option'),
|
||||
icon: 'fa fa-pen',
|
||||
button_class: 'edit-btn',
|
||||
path: edit_project_path(project),
|
||||
type: :legacy
|
||||
}
|
||||
end
|
||||
|
||||
def move_action(items)
|
||||
return unless can_manage_team?(items.first.team)
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
label: I18n.t('projects.index.move_button'),
|
||||
icon: 'fas fa-arrow-right',
|
||||
button_class: 'move-projects-btn',
|
||||
path: move_to_modal_project_folders_path,
|
||||
type: :legacy
|
||||
}
|
||||
end
|
||||
|
||||
def export_action(items)
|
||||
return unless items.all? { |item| item.is_a?(Project) ? can_export_project?(item) : true }
|
||||
|
||||
{
|
||||
name: 'export',
|
||||
label: I18n.t('projects.export_projects.export_button'),
|
||||
icon: 'fas fa-file-export',
|
||||
button_class: 'export-projects-btn',
|
||||
path: export_projects_modal_team_path(items.first.team),
|
||||
type: :legacy
|
||||
}
|
||||
end
|
||||
|
||||
def archive_action(items)
|
||||
return unless items.all? do |item|
|
||||
item.is_a?(Project) ? can_archive_project?(item) : can_manage_team?(item.team)
|
||||
end
|
||||
|
||||
{
|
||||
name: 'archive',
|
||||
label: I18n.t('projects.index.archive_button'),
|
||||
icon: 'fas fa-archive',
|
||||
button_class: 'archive-projects-btn',
|
||||
path: archive_group_projects_path,
|
||||
type: :request,
|
||||
request_method: :post
|
||||
}
|
||||
end
|
||||
|
||||
def restore_action(items)
|
||||
return unless items.all? do |item|
|
||||
item.is_a?(Project) ? can_restore_project?(item) : item.archived? && can_manage_team?(item.team)
|
||||
end
|
||||
|
||||
{
|
||||
name: 'restore',
|
||||
label: I18n.t('projects.index.restore_button'),
|
||||
icon: 'fas fa-undo',
|
||||
button_class: 'restore-projects-btn',
|
||||
path: restore_group_projects_path,
|
||||
type: :request,
|
||||
request_method: :post
|
||||
}
|
||||
end
|
||||
|
||||
def delete_folder_action(items)
|
||||
return unless items.all? do |item|
|
||||
item.is_a?(Folder) && can_delete_project_folder?(item)
|
||||
end
|
||||
|
||||
{
|
||||
name: 'delete_folders',
|
||||
label: I18n.t('general.delete'),
|
||||
icon: 'fas fa-trash',
|
||||
button_class: 'delete-folders-btn',
|
||||
path: destroy_modal_project_folders_url,
|
||||
type: :request,
|
||||
request_method: :post
|
||||
}
|
||||
end
|
||||
|
||||
def comments_action(items)
|
||||
return unless @single
|
||||
|
||||
return unless @item_type == :project
|
||||
|
||||
project = items.first
|
||||
|
||||
return unless can_read_project?(project)
|
||||
|
||||
{
|
||||
name: 'comments',
|
||||
label: I18n.t('Comments'),
|
||||
icon: 'fas fa-comment',
|
||||
button_class: 'open-comments-sidebar',
|
||||
item_type: 'Project',
|
||||
item_id: project.id,
|
||||
type: :legacy
|
||||
}
|
||||
end
|
||||
|
||||
def activities_action(items)
|
||||
return unless @single
|
||||
|
||||
return unless @item_type == :project
|
||||
|
||||
project = items.first
|
||||
|
||||
return unless can_read_project?(project)
|
||||
|
||||
activity_url_params = Activity.url_search_query({ subjects: { Project: [project] } })
|
||||
|
||||
{
|
||||
name: 'activities',
|
||||
label: I18n.t('nav.label.activities'),
|
||||
icon: 'fas fa-list',
|
||||
button_class: 'project-activities-btn',
|
||||
path: "/global_activities?#{activity_url_params}",
|
||||
type: :link
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -36,6 +36,10 @@
|
|||
<div class="table-header-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="actionToolbar" data-behaviour="vue">
|
||||
<action-toolbar actions-url="<%= actions_toolbar_projects_url %>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -59,3 +63,5 @@
|
|||
</template>
|
||||
|
||||
<%= javascript_include_tag "projects/index" %>
|
||||
|
||||
<%= javascript_include_tag "vue_components_action_toolbar" %>
|
||||
|
|
|
@ -113,52 +113,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<!--
|
||||
<a href="#" class="btn btn-light edit-btn single-object-action hidden" data-for="editable">
|
||||
<span class="fas fa-pencil-alt" aria-hidden="true"></span>
|
||||
<span class="hidden-xs"><%= t('projects.index.edit_button') %></span>
|
||||
</a>
|
||||
<a href="#" class="btn btn-light move-projects-btn multiple-object-action hidden" data-for="moveable" data-url="<%= move_to_modal_project_folders_url %>">
|
||||
<span class="fas fa-arrow-right" aria-hidden="true"></span>
|
||||
<span class="hidden-xs"><%= t('projects.index.move_button') %></span>
|
||||
</a>
|
||||
<%= button_to archive_group_projects_path,
|
||||
class: 'btn btn-light archive-projects-btn multiple-object-action project-only-action hidden',
|
||||
form_class: 'archive-projects-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('projects.index.archive_button') %></span>
|
||||
<% end %>
|
||||
<%= button_to restore_group_projects_path,
|
||||
class: 'btn btn-light restore-projects-btn multiple-object-action project-only-action hidden',
|
||||
form_class: 'restore-projects-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('projects.index.restore_button') %></span>
|
||||
<% end %>
|
||||
<%= button_to destroy_modal_project_folders_url,
|
||||
class: 'btn btn-light multiple-object-action folders-only-action hidden',
|
||||
form_class: 'delete-folders-btn',
|
||||
data: { for: :deletable },
|
||||
remote: true,
|
||||
method: :post do %>
|
||||
<span class="fas fa-trash" aria-hidden="true"></span>
|
||||
<span class="hidden-xs"><%= t('projects.index.delete_button') %></span>
|
||||
<% end %> -->
|
||||
|
||||
<!-- export projects button -->
|
||||
<!--
|
||||
<a href="#" class="btn btn-light export-projects-btn multiple-object-action hidden"
|
||||
data-export-projects-modal-url="<%= export_projects_modal_team_path(current_team) %>">
|
||||
<span class="fas fa-file-export"></span>
|
||||
<span class="hidden-xs-custom"><%= t('projects.export_projects.export_button') %></span>
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -363,6 +363,7 @@ Rails.application.routes.draw do
|
|||
post 'archive_group'
|
||||
post 'restore_group'
|
||||
put 'view_type', to: 'teams#view_type'
|
||||
get 'actions_toolbar'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,46 @@ module.exports = {
|
|||
fontFamily: {
|
||||
sans: ['Inter var', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
'sn-white': '#FFFFFF',
|
||||
'sn-super-light-grey': '#F9F9F9',
|
||||
'sn-light-grey': '#EAECF0',
|
||||
'sn-sleepy-grey': '#EAECF0',
|
||||
'sn-grey': '#98A2B3',
|
||||
'sn-dark-grey': '#475467',
|
||||
'sn-black': '#1D2939',
|
||||
'sn-blue': '#104DA9',
|
||||
'sn-science-blue': '#3B99FD',
|
||||
'sn-super-light-blue': '#F0F8FF',
|
||||
'sn-blue-hover': '#2D5FAA',
|
||||
'sn-science-blue-hover': '#79B4F3',
|
||||
'sn-alert-green': '#5EC66F',
|
||||
'sn-alert-violet': '#6F2DC1',
|
||||
'sn-alert-brittlebush': '#E9A845',
|
||||
'sn-alert-passion': '#DF3562',
|
||||
'sn-alert-turqoise': '#46C3C8',
|
||||
'sn-alert-bloo': '#3070ED',
|
||||
'sn-alert-blue-disabled': '#87A6D4',
|
||||
'sn-alert-green-disabled': '#AEE3B7',
|
||||
'sn-alert-violet-disabled': '#B796E0',
|
||||
'sn-alert-brittlebush-disabled': '#F4D3A2',
|
||||
'sn-alert-passion-disabled': '#EF9AB0',
|
||||
'sn-alert-turqoise-disabled': '#A2E1E3',
|
||||
'sn-alert-science-blue-disabled': '#9DCCFE',
|
||||
'sn-delete-red': '#CE0C24',
|
||||
'sn-delete-red-hover': '#AD0015',
|
||||
'sn-coral': '#FB565B',
|
||||
'sn-background-blue': '#DBE4F2',
|
||||
'sn-background-green': '#E7F7E9',
|
||||
'sn-background-violet': '#E9DFF6',
|
||||
'sn-background-brittlebush': '#FCF2E3',
|
||||
'sn-background-passion': '#FAE1E7',
|
||||
'sn-background-turqoise': '#E3F6F7',
|
||||
'sn-background-bloo': '#E2F0FF'
|
||||
}
|
||||
}
|
||||
},
|
||||
blocklist: [
|
||||
'collapse',
|
||||
|
|
Loading…
Reference in a new issue