mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-25 01:03:18 +08:00
Add initial table [SCI-9680]
This commit is contained in:
parent
8a0dd0258e
commit
38024d563a
19 changed files with 463 additions and 49 deletions
|
@ -568,11 +568,6 @@ li.module-hover {
|
||||||
// New projects page
|
// New projects page
|
||||||
|
|
||||||
.projects-index {
|
.projects-index {
|
||||||
--content-header-size: 9em;
|
|
||||||
.content-header {
|
|
||||||
height: var(--content-header-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-users-list {
|
.project-users-list {
|
||||||
hr {
|
hr {
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
// scss-lint:disable SelectorFormat
|
// scss-lint:disable SelectorFormat
|
||||||
|
|
||||||
.cards-wrapper {
|
.cards-wrapper {
|
||||||
--content-header-size: 9em;
|
|
||||||
--card-min-width: 200px;
|
--card-min-width: 200px;
|
||||||
--list-columns-number: 5;
|
--list-columns-number: 5;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -146,7 +146,7 @@ class LabelTemplatesController < ApplicationController
|
||||||
actions:
|
actions:
|
||||||
Toolbars::LabelTemplatesService.new(
|
Toolbars::LabelTemplatesService.new(
|
||||||
current_user,
|
current_user,
|
||||||
label_template_ids: params[:item_ids].split(',')
|
label_template_ids: JSON.parse(params[:items]).map { |i| i['id'] }
|
||||||
).actions
|
).actions
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,13 +30,21 @@ class ProjectsController < ApplicationController
|
||||||
before_action :set_current_projects_view_type, only: %i(index cards)
|
before_action :set_current_projects_view_type, only: %i(index cards)
|
||||||
layout 'fluid'
|
layout 'fluid'
|
||||||
|
|
||||||
def index; end
|
def index
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
projects = Lists::ProjectsService.new(current_team, current_user, current_folder, params).call
|
||||||
|
render json: projects, each_serializer: Lists::ProjectAndFolderSerializer, user: current_user, meta: pagination_dict(projects)
|
||||||
|
end
|
||||||
|
format.html do; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def cards
|
def cards
|
||||||
overview_service = ProjectsOverviewService.new(current_team, current_user, current_folder, params)
|
overview_service = ProjectsOverviewService.new(current_team, current_user, current_folder, params)
|
||||||
title = params[:view_mode] == 'archived' ? t('projects.index.head_title_archived') : t('projects.index.head_title')
|
title = params[:view_mode] == 'archived' ? t('projects.index.head_title_archived') : t('projects.index.head_title')
|
||||||
|
|
||||||
if filters_included?
|
if false && filters_included?
|
||||||
render json: {
|
render json: {
|
||||||
toolbar_html: render_to_string(partial: 'projects/index/toolbar'),
|
toolbar_html: render_to_string(partial: 'projects/index/toolbar'),
|
||||||
filtered: true,
|
filtered: true,
|
||||||
|
@ -385,8 +393,7 @@ class ProjectsController < ApplicationController
|
||||||
actions:
|
actions:
|
||||||
Toolbars::ProjectsService.new(
|
Toolbars::ProjectsService.new(
|
||||||
current_user,
|
current_user,
|
||||||
project_ids: params[:project_ids].split(','),
|
items: JSON.parse(params[:items]),
|
||||||
project_folder_ids: params[:project_folder_ids].split(',')
|
|
||||||
).actions
|
).actions
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||||
import LabelTemplatesTable from '../../vue/label_template/table.vue';
|
import LabelTemplatesTable from '../../vue/label_template/table.vue';
|
||||||
import { handleTurbolinks } from './helpers/turbolinks.js';
|
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||||
|
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
app.component('LabelTemplatesTable', LabelTemplatesTable);
|
app.component('LabelTemplatesTable', LabelTemplatesTable);
|
||||||
app.config.globalProperties.i18n = window.I18n;
|
app.config.globalProperties.i18n = window.I18n;
|
||||||
app.use(PerfectScrollbar);
|
app.use(PerfectScrollbar);
|
||||||
app.mount('#labelTemplatesTable');
|
mountWithTurbolinks(app, '#labelTemplatesTable');
|
||||||
handleTurbolinks(app);
|
|
||||||
|
|
||||||
|
|
11
app/javascript/packs/vue/projects_list.js
Normal file
11
app/javascript/packs/vue/projects_list.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||||
|
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||||
|
import ProjectsList from '../../vue/projects/list.vue';
|
||||||
|
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
app.component('ProjectsList', ProjectsList);
|
||||||
|
app.config.globalProperties.i18n = window.I18n;
|
||||||
|
app.use(PerfectScrollbar);
|
||||||
|
mountWithTurbolinks(app, '#ProjectsList');
|
||||||
|
|
100
app/javascript/vue/projects/list.vue
Normal file
100
app/javascript/vue/projects/list.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<DataTable :columnDefs="columnDefs"
|
||||||
|
tableId="ProjectsList"
|
||||||
|
:dataUrl="dataSource"
|
||||||
|
:reloadingTable="reloadingTable"
|
||||||
|
:toolbarActions="toolbarActions"
|
||||||
|
:actionsUrl="actionsUrl"
|
||||||
|
:withRowMenu="true"
|
||||||
|
@tableReloaded="reloadingTable = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from '../../packs/custom_axios.js';
|
||||||
|
|
||||||
|
import DataTable from '../shared/datatable/table.vue'
|
||||||
|
import UsersRenderer from './renderers/users.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectsList',
|
||||||
|
components: {
|
||||||
|
DataTable,
|
||||||
|
UsersRenderer,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
actionsUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
createUrl: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
createFolderUrl: {
|
||||||
|
type: String,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
reloadingTable: false,
|
||||||
|
columnDefs: [
|
||||||
|
{ field: "name", flex: 1, headerName: this.i18n.t('projects.index.card.name'), sortable: true, cellRenderer: this.nameRenderer },
|
||||||
|
{ field: "code", headerName: this.i18n.t('projects.index.card.id'), sortable: true },
|
||||||
|
{ field: "created_at", headerName: this.i18n.t('projects.index.card.start_date'), sortable: true },
|
||||||
|
{ field: "hidden", headerName: this.i18n.t('projects.index.card.visibility'), cellRenderer: this.visibiltyRenderer, sortable: false },
|
||||||
|
{ field: "users", headerName: this.i18n.t('projects.index.card.users'), cellRenderer: 'UsersRenderer', sortable: false, minWidth: 210 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
toolbarActions() {
|
||||||
|
let left = []
|
||||||
|
if (this.createUrl) {
|
||||||
|
left.push({
|
||||||
|
name: 'create',
|
||||||
|
icon: 'sn-icon sn-icon-new-task',
|
||||||
|
label: this.i18n.t('projects.index.new'),
|
||||||
|
type: 'emit',
|
||||||
|
path: this.createUrl,
|
||||||
|
buttonStyle: 'btn btn-primary'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.createFolderUrl) {
|
||||||
|
left.push({
|
||||||
|
name: 'create_folder',
|
||||||
|
icon: 'sn-icon sn-icon-folder',
|
||||||
|
label: this.i18n.t('projects.index.new_folder'),
|
||||||
|
type: 'emit',
|
||||||
|
path: this.createFolderUrl,
|
||||||
|
buttonStyle: 'btn btn-light',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left: left,
|
||||||
|
right: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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.name}
|
||||||
|
</a>`
|
||||||
|
},
|
||||||
|
visibiltyRenderer(params) {
|
||||||
|
if (params.data.type !== 'projects') return ''
|
||||||
|
|
||||||
|
return params.data.hidden ? this.i18n.t('projects.index.hidden') : this.i18n.t('projects.index.visible');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
39
app/javascript/vue/projects/renderers/users.vue
Normal file
39
app/javascript/vue/projects/renderers/users.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-1 cursor pointer">
|
||||||
|
<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">
|
||||||
|
+{{ 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">
|
||||||
|
<i class="sn-icon sn-icon-new-task"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UsersRenderer',
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users() {
|
||||||
|
return this.params.value || []
|
||||||
|
},
|
||||||
|
visibleUsers() {
|
||||||
|
return this.users.slice(0, 4)
|
||||||
|
},
|
||||||
|
hiddenUsers() {
|
||||||
|
return this.users.slice(4)
|
||||||
|
},
|
||||||
|
hiddenUsersTitle() {
|
||||||
|
return this.hiddenUsers.map((user) => user.full_name).join("\u000d")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -19,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'deleteStepModal',
|
name: 'confirmationModal',
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
62
app/javascript/vue/shared/datatable/row_menu_renderer.vue
Normal file
62
app/javascript/vue/shared/datatable/row_menu_renderer.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<MenuDropdown
|
||||||
|
:listItems="this.formattedList"
|
||||||
|
:btnClasses="'btn btn-light icon-btn'"
|
||||||
|
:position="'right'"
|
||||||
|
:alwaysShow="true"
|
||||||
|
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||||
|
@open="loadActions"
|
||||||
|
></MenuDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MenuDropdown from '../menu_dropdown.vue'
|
||||||
|
import axios from '../../../packs/custom_axios.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowMenuRenderer',
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
actionsMenu: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MenuDropdown
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formattedList() {
|
||||||
|
return this.actionsMenu.map((item) => {
|
||||||
|
let newItem = { text: item.label }
|
||||||
|
if (item.type == 'emit') {
|
||||||
|
newItem.emit = item.name
|
||||||
|
}
|
||||||
|
if (item.type == 'link') {
|
||||||
|
newItem.url = item.path
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadActions() {
|
||||||
|
if (this.actionsMenu.length > 0) return
|
||||||
|
|
||||||
|
axios.get(this.params.data.urls.actions)
|
||||||
|
.then((response) => {
|
||||||
|
this.actionsMenu = response.data.actions
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,21 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="relative flex flex-col flex-grow">
|
<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" />
|
||||||
<ag-grid-vue
|
<ag-grid-vue
|
||||||
class="ag-theme-alpine w-full flex-grow h-full"
|
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||||
:class="{'opacity-0': initializing}"
|
:class="{'opacity-0': initializing}"
|
||||||
:columnDefs="columnDefs"
|
:columnDefs="columnDefs"
|
||||||
:rowData="rowData"
|
:rowData="rowData"
|
||||||
:defaultColDef="defaultColDef"
|
:defaultColDef="defaultColDef"
|
||||||
:rowSelection="'multiple'"
|
:rowSelection="'multiple'"
|
||||||
|
:suppressRowTransform="true"
|
||||||
:gridOptions="gridOptions"
|
:gridOptions="gridOptions"
|
||||||
|
:suppressRowClickSelection="true"
|
||||||
@grid-ready="onGridReady"
|
@grid-ready="onGridReady"
|
||||||
@first-data-rendered="onFirstDataRendered"
|
@first-data-rendered="onFirstDataRendered"
|
||||||
@sortChanged="setOrder"
|
@sortChanged="setOrder"
|
||||||
@columnResized="saveColumnsState"
|
@columnResized="saveColumnsState"
|
||||||
@columnMoved="saveColumnsState"
|
@columnMoved="saveColumnsState"
|
||||||
@rowSelected="setSelectedRows"
|
@rowSelected="setSelectedRows"
|
||||||
|
@cellClicked="clickCell"
|
||||||
:CheckboxSelectionCallback="withCheckboxes"
|
:CheckboxSelectionCallback="withCheckboxes"
|
||||||
>
|
>
|
||||||
</ag-grid-vue>
|
</ag-grid-vue>
|
||||||
|
@ -52,6 +55,7 @@ import Pagination from './pagination.vue';
|
||||||
import CustomHeader from './tableHeader';
|
import CustomHeader from './tableHeader';
|
||||||
import ActionToolbar from './action_toolbar.vue';
|
import ActionToolbar from './action_toolbar.vue';
|
||||||
import Toolbar from './toolbar.vue';
|
import Toolbar from './toolbar.vue';
|
||||||
|
import RowMenuRenderer from './row_menu_renderer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
|
@ -60,6 +64,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
withRowMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
tableId: {
|
tableId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -108,7 +116,8 @@ export default {
|
||||||
Pagination,
|
Pagination,
|
||||||
agColumnHeader: CustomHeader,
|
agColumnHeader: CustomHeader,
|
||||||
ActionToolbar,
|
ActionToolbar,
|
||||||
Toolbar
|
Toolbar,
|
||||||
|
RowMenuRenderer
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
perPageOptions() {
|
perPageOptions() {
|
||||||
|
@ -121,7 +130,7 @@ export default {
|
||||||
},
|
},
|
||||||
actionsParams() {
|
actionsParams() {
|
||||||
return {
|
return {
|
||||||
item_ids: this.selectedRows.map(row => row.id).join(',')
|
items: JSON.stringify(this.selectedRows.map(row => { return {id: row.id, type: row.type} }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gridOptions() {
|
gridOptions() {
|
||||||
|
@ -139,7 +148,23 @@ export default {
|
||||||
checkboxSelection: true,
|
checkboxSelection: true,
|
||||||
width: 48,
|
width: 48,
|
||||||
minWidth: 48,
|
minWidth: 48,
|
||||||
resizable: false
|
resizable: false,
|
||||||
|
pinned: 'left'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.withRowMenu) {
|
||||||
|
this.columnDefs.push({
|
||||||
|
field: "rowMenu",
|
||||||
|
headerName: '',
|
||||||
|
width: 72,
|
||||||
|
minWidth: 72,
|
||||||
|
resizable: false,
|
||||||
|
sortable: false,
|
||||||
|
cellRenderer: 'RowMenuRenderer',
|
||||||
|
cellStyle: {overflow: 'visible'},
|
||||||
|
pinned: 'right'
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -159,7 +184,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatData(data) {
|
formatData(data) {
|
||||||
return data.map( (item) => Object.assign({}, item.attributes, { id: item.id }) );
|
return data.map( (item) => Object.assign({}, item.attributes, { id: item.id, type: item.type }) );
|
||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
if (this.tableState) return;
|
if (this.tableState) return;
|
||||||
|
@ -234,6 +259,11 @@ export default {
|
||||||
setSearchValue(value) {
|
setSearchValue(value) {
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
},
|
||||||
|
clickCell(e) {
|
||||||
|
if (e.column.colId !== 'rowMenu') {
|
||||||
|
e.node.setSelected(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative" v-if="listItems.length > 0" v-click-outside="closeMenu">
|
<div class="relative" v-if="listItems.length > 0 || alwaysShow" >
|
||||||
<button ref="openBtn" :class="btnClasses" @click="showMenu = !showMenu">
|
<button ref="openBtn" :class="btnClasses" @click="showMenu = !showMenu">
|
||||||
<i v-if="btnIcon" :class="btnIcon"></i>
|
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||||
{{ btnText }}
|
{{ btnText }}
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
:class="{ 'bg-sn-super-light-blue': item.active }"
|
:class="{ 'bg-sn-super-light-blue': item.active }"
|
||||||
:data-toggle="item.modalTarget && 'modal'"
|
:data-toggle="item.modalTarget && 'modal'"
|
||||||
:data-target="item.modalTarget"
|
:data-target="item.modalTarget"
|
||||||
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey"
|
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
|
||||||
@click="handleClick($event, item)"
|
@click="handleClick($event, item)"
|
||||||
>
|
>
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
:href="sub_item.url"
|
:href="sub_item.url"
|
||||||
:traget="sub_item.url_target || '_self'"
|
:traget="sub_item.url_target || '_self'"
|
||||||
:class="{ 'bg-sn-super-light-blue': item.active }"
|
:class="{ 'bg-sn-super-light-blue': item.active }"
|
||||||
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey"
|
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
|
||||||
@click="handleClick($event, sub_item)"
|
@click="handleClick($event, sub_item)"
|
||||||
>
|
>
|
||||||
{{ sub_item.text }}
|
{{ sub_item.text }}
|
||||||
|
@ -75,6 +75,7 @@ export default {
|
||||||
btnText: { type: String, required: false },
|
btnText: { type: String, required: false },
|
||||||
btnIcon: { type: String, required: false },
|
btnIcon: { type: String, required: false },
|
||||||
caret: { type: Boolean, default: false },
|
caret: { type: Boolean, default: false },
|
||||||
|
alwaysShow: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -93,6 +94,7 @@ export default {
|
||||||
this.$refs.flyout.style.marginBottom = `${this.$refs.openBtn.offsetHeight}px`;
|
this.$refs.flyout.style.marginBottom = `${this.$refs.openBtn.offsetHeight}px`;
|
||||||
this.updateOpenDirectoin();
|
this.updateOpenDirectoin();
|
||||||
})
|
})
|
||||||
|
this.$emit('open');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,6 +39,11 @@ class UserAssignment < ApplicationRecord
|
||||||
user_assignments.none?
|
user_assignments.none?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_name_with_role
|
||||||
|
"#{user.name} - #{user_role.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_assignable_team
|
def set_assignable_team
|
||||||
|
|
51
app/serializers/lists/project_and_folder_serializer.rb
Normal file
51
app/serializers/lists/project_and_folder_serializer.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
module Lists
|
||||||
|
class ProjectAndFolderSerializer < ActiveModel::Serializer
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
attributes :name, :code, :created_at, :archived_on, :users, :hidden, :urls, :folder
|
||||||
|
|
||||||
|
def folder
|
||||||
|
!project?
|
||||||
|
end
|
||||||
|
|
||||||
|
def code
|
||||||
|
object.code if project?
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_at
|
||||||
|
I18n.l(object.created_at, format: :full) if project?
|
||||||
|
end
|
||||||
|
|
||||||
|
def archived_on
|
||||||
|
I18n.l(object.archived_on, format: :full) if project? && object.archived_on
|
||||||
|
end
|
||||||
|
|
||||||
|
def hidden
|
||||||
|
object.hidden? if project?
|
||||||
|
end
|
||||||
|
|
||||||
|
def users
|
||||||
|
if project?
|
||||||
|
object.user_assignments.map do |ua|
|
||||||
|
{
|
||||||
|
avatar: avatar_path(ua.user, :icon_small),
|
||||||
|
full_name: ua.user_name_with_role
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def project?
|
||||||
|
object.class == Project
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
110
app/services/lists/projects_service.rb
Normal file
110
app/services/lists/projects_service.rb
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
module Lists
|
||||||
|
class ProjectsService < BaseService
|
||||||
|
def initialize(team, user, folder, params)
|
||||||
|
@team = team
|
||||||
|
@user = user
|
||||||
|
@current_folder = folder
|
||||||
|
@params = params
|
||||||
|
@view_mode = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
projects = fetch_projects
|
||||||
|
folders = fetch_project_folders
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
records = projects + folders
|
||||||
|
|
||||||
|
records = sort_records(records)
|
||||||
|
paginate_records(records)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_projects
|
||||||
|
@team.projects
|
||||||
|
.includes(:team, user_assignments: %i(user user_role))
|
||||||
|
.includes(:project_comments, experiments: { my_modules: { my_module_status: :my_module_status_implications } })
|
||||||
|
.visible_to(@user, @team)
|
||||||
|
.left_outer_joins(:project_comments)
|
||||||
|
.select('projects.*')
|
||||||
|
.select('COUNT(DISTINCT comments.id) AS comment_count')
|
||||||
|
.group('projects.id')
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_project_folders
|
||||||
|
project_folders = @team.project_folders
|
||||||
|
.includes(:team)
|
||||||
|
.joins('LEFT OUTER JOIN project_folders child_folders
|
||||||
|
ON child_folders.parent_folder_id = project_folders.id')
|
||||||
|
.left_outer_joins(:projects)
|
||||||
|
project_folders.select('project_folders.*')
|
||||||
|
.select('COUNT(DISTINCT projects.id) AS projects_count')
|
||||||
|
.select('COUNT(DISTINCT child_folders.id) AS folders_count')
|
||||||
|
.group('project_folders.id')
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_project_records(records)
|
||||||
|
return records
|
||||||
|
|
||||||
|
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])
|
||||||
|
end
|
||||||
|
if @params[:members].present?
|
||||||
|
records = records.joins(:user_assignments).where(user_assignments: { user_id: @params[: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])
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_records(records)
|
||||||
|
return records unless @params[:order]
|
||||||
|
|
||||||
|
sort = "#{order_params[:column]}_#{sort_direction(order_params)}"
|
||||||
|
|
||||||
|
case sort
|
||||||
|
when 'created_at_ASC'
|
||||||
|
records.sort_by(&:created_at).reverse!
|
||||||
|
when 'created_at_DESC'
|
||||||
|
records.sort_by(&:created_at)
|
||||||
|
when 'name_ASC'
|
||||||
|
records.sort_by { |c| c.name.downcase }
|
||||||
|
when 'name_DESC'
|
||||||
|
records.sort_by { |c| c.name.downcase }.reverse!
|
||||||
|
when 'code_ASC'
|
||||||
|
records.sort_by(&:id)
|
||||||
|
when 'code_DESC'
|
||||||
|
records.sort_by(&:id).reverse!
|
||||||
|
when 'archived_on_ASC'
|
||||||
|
records.sort_by(&:archived_on)
|
||||||
|
when 'archived_on_DESC'
|
||||||
|
records.sort_by(&:archived_on).reverse!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginate_records(records)
|
||||||
|
Kaminari.paginate_array(records).page(@params[:page]).per(@params[:per_page])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,8 +7,11 @@ module Toolbars
|
||||||
include Canaid::Helpers::PermissionsHelper
|
include Canaid::Helpers::PermissionsHelper
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
def initialize(current_user, project_ids: [], project_folder_ids: [])
|
def initialize(current_user, items: [])
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
|
project_ids = items.select { |i| i['type'] == 'projects' }.map { |i| i['id'] }
|
||||||
|
project_folder_ids = items.select { |i| i['type'] == 'project_folders' }.map { |i| i['id'] }
|
||||||
|
|
||||||
@projects = current_user.current_team.projects.where(id: project_ids)
|
@projects = current_user.current_team.projects.where(id: project_ids)
|
||||||
@project_folders = current_user.current_team.project_folders.where(id: project_folder_ids)
|
@project_folders = current_user.current_team.project_folders.where(id: project_folder_ids)
|
||||||
|
|
||||||
|
@ -59,7 +62,7 @@ module Toolbars
|
||||||
icon: 'sn-icon sn-icon-edit',
|
icon: 'sn-icon sn-icon-edit',
|
||||||
button_class: 'edit-btn',
|
button_class: 'edit-btn',
|
||||||
path: edit_project_path(project),
|
path: edit_project_path(project),
|
||||||
type: :legacy
|
type: :emit
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
project_folder = @items.first
|
project_folder = @items.first
|
||||||
|
@ -72,7 +75,7 @@ module Toolbars
|
||||||
icon: 'sn-icon sn-icon-edit',
|
icon: 'sn-icon sn-icon-edit',
|
||||||
button_class: 'edit-btn',
|
button_class: 'edit-btn',
|
||||||
path: edit_project_folder_path(project_folder),
|
path: edit_project_folder_path(project_folder),
|
||||||
type: :legacy
|
type: :emit
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -96,9 +99,8 @@ module Toolbars
|
||||||
name: 'access',
|
name: 'access',
|
||||||
label: I18n.t('general.access'),
|
label: I18n.t('general.access'),
|
||||||
icon: 'sn-icon sn-icon-project-member-access',
|
icon: 'sn-icon sn-icon-project-member-access',
|
||||||
button_class: 'access-btn',
|
|
||||||
path: path,
|
path: path,
|
||||||
type: 'remote-modal'
|
type: :emit
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -110,9 +112,8 @@ module Toolbars
|
||||||
name: 'move',
|
name: 'move',
|
||||||
label: I18n.t('projects.index.move_button'),
|
label: I18n.t('projects.index.move_button'),
|
||||||
icon: 'sn-icon sn-icon-move',
|
icon: 'sn-icon sn-icon-move',
|
||||||
button_class: 'move-projects-btn',
|
|
||||||
path: move_to_modal_project_folders_path,
|
path: move_to_modal_project_folders_path,
|
||||||
type: :legacy
|
type: :emit
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -123,9 +124,8 @@ module Toolbars
|
||||||
name: 'export',
|
name: 'export',
|
||||||
label: I18n.t('projects.export_projects.export_button'),
|
label: I18n.t('projects.export_projects.export_button'),
|
||||||
icon: 'sn-icon sn-icon-export',
|
icon: 'sn-icon sn-icon-export',
|
||||||
button_class: 'export-projects-btn',
|
|
||||||
path: export_projects_modal_team_path(@items.first.team),
|
path: export_projects_modal_team_path(@items.first.team),
|
||||||
type: :legacy
|
type: :emit
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -138,10 +138,8 @@ module Toolbars
|
||||||
name: 'archive',
|
name: 'archive',
|
||||||
label: I18n.t('projects.index.archive_button'),
|
label: I18n.t('projects.index.archive_button'),
|
||||||
icon: 'sn-icon sn-icon-archive',
|
icon: 'sn-icon sn-icon-archive',
|
||||||
button_class: 'archive-projects-btn',
|
|
||||||
path: archive_group_projects_path,
|
path: archive_group_projects_path,
|
||||||
type: :request,
|
type: :emit,
|
||||||
request_method: :post
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -156,8 +154,7 @@ module Toolbars
|
||||||
icon: 'sn-icon sn-icon-restore',
|
icon: 'sn-icon sn-icon-restore',
|
||||||
button_class: 'restore-projects-btn',
|
button_class: 'restore-projects-btn',
|
||||||
path: restore_group_projects_path,
|
path: restore_group_projects_path,
|
||||||
type: :request,
|
type: :emit
|
||||||
request_method: :post
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -170,9 +167,8 @@ module Toolbars
|
||||||
name: 'delete_folders',
|
name: 'delete_folders',
|
||||||
label: I18n.t('general.delete'),
|
label: I18n.t('general.delete'),
|
||||||
icon: 'sn-icon sn-icon-delete',
|
icon: 'sn-icon sn-icon-delete',
|
||||||
button_class: 'delete-folders-btn',
|
|
||||||
path: destroy_modal_project_folders_path(project_folder_ids: @items.map(&:id)),
|
path: destroy_modal_project_folders_path(project_folder_ids: @items.map(&:id)),
|
||||||
type: 'remote-modal'
|
type: :emit
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -189,10 +185,7 @@ module Toolbars
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
label: I18n.t('Comments'),
|
label: I18n.t('Comments'),
|
||||||
icon: 'sn-icon sn-icon-comments',
|
icon: 'sn-icon sn-icon-comments',
|
||||||
button_class: 'open-comments-sidebar',
|
type: :emit
|
||||||
item_type: 'Project',
|
|
||||||
item_id: project.id,
|
|
||||||
type: :legacy
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,20 @@
|
||||||
<div id="projectsWrapper" class="content-pane flexible projects-index <%= projects_view_mode %>" data-view-mode="<%= projects_view_mode %>" data-e2e="e2e-projects-container">
|
<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} %>
|
<%= render partial: 'projects/index/header', locals: { current_folder: current_folder} %>
|
||||||
|
|
||||||
|
<div id="ProjectsList" class="fixed-content-body">
|
||||||
|
<projects-list
|
||||||
|
actions-url="<%= actions_toolbar_projects_url %>"
|
||||||
|
data-source="<%= projects_path(format: :json) %>"
|
||||||
|
create-url="<%= projects_path if can_create_projects?(current_team) %>"
|
||||||
|
create-folder-url="<%= project_folders_path if can_create_project_folders?(current_team) %>"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<%= javascript_include_tag 'vue_projects_list' %>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="toolbarWrapper" class="toolbar-row" data-width-breakpoint="750">
|
||||||
|
<%= render partial: 'projects/index/toolbar' %>
|
||||||
|
</div>
|
||||||
<span style="display: none;" data-hook="projects-index-html"></span>
|
<span style="display: none;" data-hook="projects-index-html"></span>
|
||||||
|
|
||||||
<%= render partial: 'projects/index/modals/edit_modal' %>
|
<%= render partial: 'projects/index/modals/edit_modal' %>
|
||||||
|
|
|
@ -19,7 +19,4 @@
|
||||||
<span class="projects-title name-readonly-placeholder"><%= current_folder&.name || t('projects.index.head_title_archived') %></span>
|
<span class="projects-title name-readonly-placeholder"><%= current_folder&.name || t('projects.index.head_title_archived') %></span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbarWrapper" class="toolbar-row" data-width-breakpoint="750">
|
|
||||||
<%= render partial: 'projects/index/toolbar' %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,8 @@ const entryList = {
|
||||||
vue_components_export_stock_consumption_modal: './app/javascript/packs/vue/export_stock_consumption_modal.js',
|
vue_components_export_stock_consumption_modal: './app/javascript/packs/vue/export_stock_consumption_modal.js',
|
||||||
vue_components_manage_stock_value_modal: './app/javascript/packs/vue/manage_stock_value_modal.js',
|
vue_components_manage_stock_value_modal: './app/javascript/packs/vue/manage_stock_value_modal.js',
|
||||||
vue_legacy_datetime_picker: './app/javascript/packs/vue/legacy/datetime_picker.js',
|
vue_legacy_datetime_picker: './app/javascript/packs/vue/legacy/datetime_picker.js',
|
||||||
vue_label_templates_table: './app/javascript/packs/vue/label_templates_table.js'
|
vue_label_templates_table: './app/javascript/packs/vue/label_templates_table.js',
|
||||||
|
vue_projects_list: './app/javascript/packs/vue/projects_list.js',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||||
|
|
Loading…
Reference in a new issue