mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-24 08:43:13 +08:00
Replace project table [SCI-9680]
This commit is contained in:
parent
38024d563a
commit
7fdbc73098
14 changed files with 185 additions and 48 deletions
|
@ -393,7 +393,7 @@ class ProjectsController < ApplicationController
|
|||
actions:
|
||||
Toolbars::ProjectsService.new(
|
||||
current_user,
|
||||
items: JSON.parse(params[:items]),
|
||||
items: JSON.parse(params[:items])
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
:withRowMenu="true"
|
||||
:activePageUrl="activePageUrl"
|
||||
:archivedPageUrl="archivedPageUrl"
|
||||
:currentViewMode="currentViewMode"
|
||||
:filters="filters"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
/>
|
||||
</div>
|
||||
|
@ -38,6 +42,16 @@ export default {
|
|||
},
|
||||
createFolderUrl: {
|
||||
type: String,
|
||||
},
|
||||
activePageUrl: {
|
||||
type: String,
|
||||
},
|
||||
archivedPageUrl: {
|
||||
type: String,
|
||||
},
|
||||
currentViewMode: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -79,13 +93,40 @@ export default {
|
|||
left: left,
|
||||
right: []
|
||||
}
|
||||
},
|
||||
filters() {
|
||||
let filters = [{
|
||||
key: 'query',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
type: 'DateRange',
|
||||
label: this.i18n.t("filters_modal.created_on.label"),
|
||||
}]
|
||||
|
||||
if (this.currentViewMode === 'archived') {
|
||||
filters.push({
|
||||
key: 'archived_at',
|
||||
type: 'DateRange',
|
||||
label: this.i18n.t("filters_modal.archived_on.label"),
|
||||
})
|
||||
}
|
||||
|
||||
filters.push({
|
||||
key: 'folder_search',
|
||||
type: 'Checkbox',
|
||||
label: this.i18n.t("projects.index.filters_modal.folders.label"),
|
||||
})
|
||||
|
||||
return filters
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
nameRenderer(params) {
|
||||
let showUrl = params.data.urls.show;
|
||||
return `<a href="${showUrl}" class="flex items-center gap-1">
|
||||
${params.data.folder ? 'sn-icon mini sn-icon-mini-folder-left' : ''}
|
||||
${params.data.folder ? '<i class="sn-icon mini sn-icon-mini-folder-left"></i>' : ''}
|
||||
${params.data.name}
|
||||
</a>`
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="flex items-center gap-1 cursor pointer">
|
||||
<div v-if="!params.data.folder" class="flex items-center gap-1 cursor pointer h-10">
|
||||
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
|
||||
<img :src="user.avatar" class="w-7 h-7" />
|
||||
</div>
|
||||
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle" class="flex shrink-0 items-center justify-center w-7 h-7 rounded-full bg-sn-dark-grey text-sn-white">
|
||||
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle" class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
|
||||
+{{ hiddenUsers.length }}
|
||||
</div>
|
||||
<div class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full bg-sn-light-grey text-sn-dark-grey">
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="relative flex flex-col flex-grow z-10">
|
||||
<Toolbar :toolbarActions="toolbarActions" @toolbar:action="emitAction" :searchValue="searchValue" @search:change="setSearchValue" />
|
||||
<Toolbar
|
||||
:toolbarActions="toolbarActions"
|
||||
@toolbar:action="emitAction"
|
||||
:searchValue="searchValue"
|
||||
@search:change="setSearchValue"
|
||||
:activePageUrl="activePageUrl"
|
||||
:archivedPageUrl="archivedPageUrl"
|
||||
:currentViewMode="currentViewMode"
|
||||
:filters="filters"
|
||||
@applyFilters="applyFilters"
|
||||
/>
|
||||
<ag-grid-vue
|
||||
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||
:class="{'opacity-0': initializing}"
|
||||
|
@ -90,7 +100,22 @@ export default {
|
|||
reloadingTable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
activePageUrl: {
|
||||
type: String,
|
||||
},
|
||||
archivedPageUrl: {
|
||||
type: String,
|
||||
},
|
||||
currentViewMode: {
|
||||
type: String,
|
||||
default: 'active'
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -106,7 +131,8 @@ export default {
|
|||
totalPage: 0,
|
||||
selectedRows: [],
|
||||
searchValue: '',
|
||||
initializing: true
|
||||
initializing: true,
|
||||
activeFilters: {}
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
@ -198,7 +224,9 @@ export default {
|
|||
per_page: this.perPage,
|
||||
page: this.page,
|
||||
order: this.order,
|
||||
search: this.searchValue
|
||||
search: this.searchValue,
|
||||
view_mode: this.currentViewMode,
|
||||
filters: this.activeFilters
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
|
@ -264,6 +292,10 @@ export default {
|
|||
if (e.column.colId !== 'rowMenu') {
|
||||
e.node.setSelected(true);
|
||||
}
|
||||
},
|
||||
applyFilters(filters) {
|
||||
this.activeFilters = filters;
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="flex py-4 items-center">
|
||||
<div class="flex py-4 items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<a v-for="action in toolbarActions.left" :key="action.label"
|
||||
:class="action.buttonStyle"
|
||||
|
@ -9,7 +9,19 @@
|
|||
{{ action.label }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<div>
|
||||
<div v-if="archivedPageUrl" class="flex items-center gap-4">
|
||||
<MenuDropdown
|
||||
:listItems="this.viewModes"
|
||||
:btnClasses="'btn btn-light icon-btn'"
|
||||
:btnText="i18n.t(`projects.index.${currentViewMode}`)"
|
||||
:caret="true"
|
||||
:position="'right'"
|
||||
@open="loadActions"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a v-for="action in toolbarActions.right" :key="action.label"
|
||||
:class="action.buttonStyle"
|
||||
:href="action.path"
|
||||
|
@ -17,7 +29,7 @@
|
|||
<i :class="action.icon"></i>
|
||||
{{ action.label }}
|
||||
</a>
|
||||
<div class="sci-input-container-v2" :class="{'w-48': showSearch, 'w-11': !showSearch}">
|
||||
<div v-if="filters.length == 0" class="sci-input-container-v2" :class="{'w-48': showSearch, 'w-11': !showSearch}">
|
||||
<input
|
||||
ref="searchInput"
|
||||
class="sci-input-field !pr-8"
|
||||
|
@ -30,11 +42,15 @@
|
|||
/>
|
||||
<i class="sn-icon sn-icon-search !m-2.5 !ml-auto right-0"></i>
|
||||
</div>
|
||||
<FilterDropdown v-else :filters="filters" @applyFilters="applyFilters" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuDropdown from '../menu_dropdown.vue'
|
||||
import FilterDropdown from '../filters/filter_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'Toolbar',
|
||||
props: {
|
||||
|
@ -44,6 +60,32 @@ export default {
|
|||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
},
|
||||
activePageUrl: {
|
||||
type: String,
|
||||
},
|
||||
archivedPageUrl: {
|
||||
type: String,
|
||||
},
|
||||
currentViewMode: {
|
||||
type: String,
|
||||
default: 'active'
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MenuDropdown,
|
||||
FilterDropdown
|
||||
},
|
||||
computed: {
|
||||
viewModes() {
|
||||
return [
|
||||
{ text: this.i18n.t('projects.index.active'), url: this.activePageUrl},
|
||||
{ text: this.i18n.t('projects.index.archived'), url: this.archivedPageUrl }
|
||||
]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -76,7 +118,10 @@ export default {
|
|||
case 'link':
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
applyFilters(filters) {
|
||||
this.$emit('applyFilters', filters);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sci-flyout-body max-h-[400px] overflow-y-auto perfect-scrollbar relative w-[calc(100%_+_1.125rem)] pr-5">
|
||||
<div class="sci-flyout-body max-h-[400px] relative w-[calc(100%_+_1.125rem)] pr-5">
|
||||
<div v-for="filter in filters" :key="filter.key + resetFilters" class="">
|
||||
<Component :is="`${filter.type}Filter`" :filter="filter" :value="filterValues[filter.key]" @update="updateFilter" />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<div class="mb-6">TODO</div>
|
||||
<div class="mb-6 flex gap-2 items-center">
|
||||
<span class="sci-checkbox-container">
|
||||
<input type="checkbox" v-model="value" class="sci-checkbox" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</span>
|
||||
<span class="sci-label">{{ filter.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -8,7 +14,15 @@
|
|||
props: {
|
||||
filter: { type: Object, required: true }
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
data: function() {
|
||||
return {
|
||||
value: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function() {
|
||||
this.$emit('update', { key: this.filter.key, value: this.value });
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-6 flex-col">
|
||||
<div class="w-full">
|
||||
<label class="sci-label">{{ filter.label }} ({{ i18n.t('general.from') }})</label>
|
||||
<div class="flex flex-col">
|
||||
<label class="sci-label">{{ filter.label }}</label>
|
||||
<div class="w-full mb-2">
|
||||
<DateTimePicker
|
||||
class="w-full"
|
||||
@change="updateDateFrom"
|
||||
|
@ -12,7 +12,6 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label class="sci-label">{{ filter.label }} ({{ i18n.t('general.to') }})</label>
|
||||
<DateTimePicker
|
||||
class="w-full"
|
||||
@change="updateDateTo"
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<template>
|
||||
<div class="mb-6">
|
||||
<inputWithHistory :modelValue="value" :label="filter.label" @update:modelValue="update" :placeholder="filter.placeholder" :id="'textSearch' + filter.key"/>
|
||||
<inputWithHistory
|
||||
:modelValue="value"
|
||||
:label="filter.label || i18n.t('filters_modal.text.label')"
|
||||
@update:modelValue="update"
|
||||
:placeholder="filter.placeholder || i18n.t('filters_modal.text.placeholder')"
|
||||
:id="'textSearch' + filter.key"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="relative" v-if="listItems.length > 0 || alwaysShow" >
|
||||
<div class="relative" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
|
||||
<button ref="openBtn" :class="btnClasses" @click="showMenu = !showMenu">
|
||||
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||
{{ btnText }}
|
||||
|
|
|
@ -43,7 +43,6 @@ class UserAssignment < ApplicationRecord
|
|||
"#{user.name} - #{user_role.name}"
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def set_assignable_team
|
||||
|
|
|
@ -38,14 +38,15 @@ module Lists
|
|||
def urls
|
||||
{
|
||||
show: project? ? project_path(object) : project_folder_path(object),
|
||||
actions: actions_toolbar_projects_path(items: [{id: object.id, type: project? ? 'projects' : 'project_folders'}].to_json)
|
||||
actions: actions_toolbar_projects_path(items: [{ id: object.id,
|
||||
type: project? ? 'projects' : 'project_folders' }].to_json)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project?
|
||||
object.class == Project
|
||||
object.instance_of?(Project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ module Lists
|
|||
@user = user
|
||||
@current_folder = folder
|
||||
@params = params
|
||||
@view_mode = ''
|
||||
@filters = params[:filters] || {}
|
||||
end
|
||||
|
||||
def call
|
||||
|
@ -15,10 +15,14 @@ module Lists
|
|||
projects = filter_project_records(projects)
|
||||
folders = filter_project_folder_records(folders)
|
||||
|
||||
projects = projects.where(project_folder: @current_folder)
|
||||
folders = folders.where(parent_folder: @current_folder)
|
||||
if @filters[:folder_search].present? && @filters[:folder_search] == 'true'
|
||||
records = projects
|
||||
else
|
||||
projects = projects.where(project_folder: @current_folder)
|
||||
folders = folders.where(parent_folder: @current_folder)
|
||||
|
||||
records = projects + folders
|
||||
records = projects + folders
|
||||
end
|
||||
|
||||
records = sort_records(records)
|
||||
paginate_records(records)
|
||||
|
@ -50,31 +54,29 @@ module Lists
|
|||
end
|
||||
|
||||
def filter_project_records(records)
|
||||
return records
|
||||
records = records.archived if @params[:view_mode] == 'archived'
|
||||
records = records.active if @params[:view_mode] == 'active'
|
||||
|
||||
records = records.archived if @view_mode == 'archived'
|
||||
records = records.active if @view_mode == 'active'
|
||||
if @params[:search].present?
|
||||
records = records.where_attributes_like(['projects.name', Project::PREFIXED_ID_SQL], @params[:search])
|
||||
if @filters[:query].present?
|
||||
records = records.where_attributes_like(['projects.name', Project::PREFIXED_ID_SQL], @filters[:query])
|
||||
end
|
||||
if @params[:members].present?
|
||||
records = records.joins(:user_assignments).where(user_assignments: { user_id: @params[:members] })
|
||||
|
||||
if @filters[:members].present?
|
||||
records = records.joins(:user_assignments).where(user_assignments: { user_id: @filters[:members] })
|
||||
end
|
||||
records = records.where('projects.created_at > ?', @params[:created_on_from]) if @params[:created_on_from].present?
|
||||
records = records.where('projects.created_at < ?', @params[:created_on_to]) if @params[:created_on_to].present?
|
||||
records = records.where('projects.archived_on < ?', @params[:archived_on_to]) if @params[:archived_on_to].present?
|
||||
if @params[:archived_on_from].present?
|
||||
records = records.where('projects.archived_on > ?', @params[:archived_on_from])
|
||||
records = records.where('projects.created_at > ?', @filters[:created_on_from]) if @filters[:created_on_from].present?
|
||||
records = records.where('projects.created_at < ?', @filters[:created_on_to]) if @filters[:created_on_to].present?
|
||||
records = records.where('projects.archived_on < ?', @filters[:archived_on_to]) if @filters[:archived_on_to].present?
|
||||
if @filters[:archived_on_from].present?
|
||||
records = records.where('projects.archived_on > ?', @filters[:archived_on_from])
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
def filter_project_folder_records(records)
|
||||
return records
|
||||
|
||||
records = records.archived if @view_mode == 'archived'
|
||||
records = records.active if @view_mode == 'active'
|
||||
records = records.where_attributes_like('project_folders.name', @params[:search]) if @params[:search].present?
|
||||
records = records.where_attributes_like('project_folders.name', @filters[:query]) if @filters[:query].present?
|
||||
records
|
||||
end
|
||||
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
<% provide(:sidebar_url, sidebar_team_path(current_team, project_folder_id: current_folder&.id)) %>
|
||||
<% provide(:container_class, 'no-second-nav-container') %>
|
||||
|
||||
<%= content_for :sidebar do %>
|
||||
<%= render partial: 'shared/sidebar/projects', locals: { team: current_team, sort: nil, view_mode: projects_view_mode } if current_team %>
|
||||
<% end %>
|
||||
|
||||
<div id="projectsWrapper" class="content-pane flexible projects-index <%= projects_view_mode %>" data-view-mode="<%= projects_view_mode %>" data-e2e="e2e-projects-container">
|
||||
<%= render partial: 'projects/index/header', locals: { current_folder: current_folder} %>
|
||||
|
||||
|
@ -13,6 +9,9 @@
|
|||
<projects-list
|
||||
actions-url="<%= actions_toolbar_projects_url %>"
|
||||
data-source="<%= projects_path(format: :json) %>"
|
||||
active-page-url="<%= projects_path(view_mode: :active) %>"
|
||||
archived-page-url="<%= projects_path(view_mode: :archived) %>"
|
||||
current-view-mode="<%= params[:view_mode] || :active %>"
|
||||
create-url="<%= projects_path if can_create_projects?(current_team) %>"
|
||||
create-folder-url="<%= project_folders_path if can_create_project_folders?(current_team) %>"
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue