mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-15 21:56:12 +08:00
Merge pull request #6742 from aignatov-bio/ai-sci-9680-replace-projects-table
Finalize projects table
This commit is contained in:
commit
5acec04794
32 changed files with 996 additions and 300 deletions
|
@ -1,6 +1,9 @@
|
|||
$flyout-shadow: 0px 1px 4px rgba(35, 31, 32, 0.15);
|
||||
$modal-shadow: 0px 4px 16px rgba(35, 31, 32, 0.15);
|
||||
|
||||
.sn-shadow-flyout {
|
||||
box-shadow: $flyout-shadow;
|
||||
}
|
||||
|
||||
.sn-shadow-menu-sm {
|
||||
box-shadow: 0px 16px 32px 0px rgba(16, 24, 40, 0.07);
|
||||
|
|
|
@ -77,4 +77,13 @@
|
|||
.sci-input-container-v2 .history-flyout li:hover {
|
||||
@apply bg-sn-super-light-grey;
|
||||
}
|
||||
|
||||
.sci-input-container-v2.error input {
|
||||
@apply border-sn-alert-passion;
|
||||
}
|
||||
|
||||
.sci-input-container-v2.error::after {
|
||||
@apply absolute -bottom-5 text-sn-alert-passion text-xs;
|
||||
content: attr(data-error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ class ProjectFoldersController < ApplicationController
|
|||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_manage_permissions, only: %i(archive move_to)
|
||||
|
||||
def tree
|
||||
render json: folders_tree(current_team, current_user)
|
||||
end
|
||||
|
||||
def new
|
||||
@project_folder =
|
||||
current_team.project_folders.new(parent_folder: current_folder, archived: projects_view_mode_archived?)
|
||||
|
@ -48,11 +52,11 @@ class ProjectFoldersController < ApplicationController
|
|||
move_projects(destination_folder)
|
||||
move_folders(destination_folder)
|
||||
end
|
||||
render json: { flash: I18n.t('projects.move.success_flash') }
|
||||
render json: { message: I18n.t('projects.move.success_flash') }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
render json: { flash: I18n.t('projects.move.error_flash') }, status: :bad_request
|
||||
render json: { error: I18n.t('projects.move.error_flash') }, status: :bad_request
|
||||
end
|
||||
|
||||
def move_to_modal
|
||||
|
@ -109,7 +113,7 @@ class ProjectFoldersController < ApplicationController
|
|||
if counter.positive?
|
||||
render json: { message: t('projects.delete_folders.success_flash', number: counter) }
|
||||
else
|
||||
render json: { message: t('projects.delete_folders.error_flash') }, status: :unprocessable_entity
|
||||
render json: { error: t('projects.delete_folders.error_flash') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -132,13 +136,9 @@ class ProjectFoldersController < ApplicationController
|
|||
end
|
||||
|
||||
def move_params
|
||||
parsed_params = ActionController::Parameters.new(
|
||||
movables: JSON.parse(params[:movables]),
|
||||
destination_folder_id: params[:destination_folder_id]
|
||||
)
|
||||
parsed_params.require(:destination_folder_id)
|
||||
parsed_params.require(:movables)
|
||||
parsed_params.permit(:destination_folder_id, movables: %i(id type))
|
||||
params.require(:destination_folder_id)
|
||||
params.require(:movables)
|
||||
params.permit(:destination_folder_id, movables: %i(id type))
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
|
@ -150,7 +150,7 @@ class ProjectFoldersController < ApplicationController
|
|||
end
|
||||
|
||||
def move_projects(destination_folder)
|
||||
project_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'project' }.compact
|
||||
project_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'projects' }.compact
|
||||
return if project_ids.blank?
|
||||
|
||||
current_team.projects.where(id: project_ids).each do |project|
|
||||
|
@ -167,7 +167,7 @@ class ProjectFoldersController < ApplicationController
|
|||
end
|
||||
|
||||
def move_folders(destination_folder)
|
||||
folder_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'project_folder' }.compact
|
||||
folder_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'project_folders' }.compact
|
||||
return if folder_ids.blank?
|
||||
|
||||
current_team.project_folders.where(id: folder_ids).each do |folder|
|
||||
|
|
|
@ -8,6 +8,7 @@ class ProjectsController < ApplicationController
|
|||
include CardsViewHelper
|
||||
include ExperimentsHelper
|
||||
include Breadcrumbs
|
||||
include UserRolesHelper
|
||||
|
||||
attr_reader :current_folder
|
||||
|
||||
|
@ -19,7 +20,7 @@ class ProjectsController < ApplicationController
|
|||
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 inventory_assigning_project_filter
|
||||
actions_toolbar)
|
||||
actions_toolbar user_roles)
|
||||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_manage_permissions, only: :edit
|
||||
before_action :load_exp_sort_var, only: :show
|
||||
|
@ -363,10 +364,10 @@ class ProjectsController < ApplicationController
|
|||
|
||||
def users_filter
|
||||
users = current_team.users.search(false, params[:query]).map do |u|
|
||||
{ value: u.id, label: escape_input(u.name), params: { avatar_url: avatar_path(u, :icon_small) } }
|
||||
[u.id, u.name, { avatar_url: avatar_path(u, :icon_small) }]
|
||||
end
|
||||
|
||||
render json: users, status: :ok
|
||||
render json: { data: users }, status: :ok
|
||||
end
|
||||
|
||||
def view_type
|
||||
|
@ -388,6 +389,11 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def user_roles
|
||||
render json: { data: user_roles_collection(Project.new).map(&:reverse) }
|
||||
end
|
||||
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions:
|
||||
|
|
|
@ -30,34 +30,13 @@ module ProjectsHelper
|
|||
conns.to_s[1..-2]
|
||||
end
|
||||
|
||||
def sidebar_folders_tree(team, user, sort, folders_only: false)
|
||||
def folders_tree(team, user)
|
||||
sort ||= team.current_view_state(user).state.dig('projects', 'active', 'sort')
|
||||
if projects_view_mode_archived?
|
||||
records = ProjectFolder.archived.inner_folders(team)
|
||||
records += team.projects.archived.visible_to(user, team) unless folders_only
|
||||
records = ProjectFolder.archived.inner_folders(team).order(:name).select(:id, :name, :parent_folder_id)
|
||||
else
|
||||
records = ProjectFolder.active.inner_folders(team)
|
||||
records += team.projects.active.visible_to(user, team) unless folders_only
|
||||
sort = 'new' if %w(archived_old archived_new).include?(sort)
|
||||
records = ProjectFolder.active.inner_folders(team).order(:name).select(:id, :name, :parent_folder_id)
|
||||
end
|
||||
records = case sort
|
||||
when 'new'
|
||||
records.sort_by(&:created_at).reverse!
|
||||
when 'old'
|
||||
records.sort_by(&:created_at)
|
||||
when 'atoz'
|
||||
records.sort_by { |c| c.name.downcase }
|
||||
when 'ztoa'
|
||||
records.sort_by { |c| c.name.downcase }.reverse!
|
||||
when 'id_asc'
|
||||
records.sort_by(&:id)
|
||||
when 'id_desc'
|
||||
records.sort_by(&:id).reverse!
|
||||
when 'archived_old'
|
||||
records.sort_by(&:archived_on)
|
||||
when 'archived_new'
|
||||
records.sort_by(&:archived_on).reverse!
|
||||
end
|
||||
folders_recursive_builder(nil, records)
|
||||
end
|
||||
|
||||
|
|
76
app/javascript/vue/projects/card.vue
Normal file
76
app/javascript/vue/projects/card.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div v-if="!params.folder" class="p-4 rounded sn-shadow-flyout flex flex-col">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="sci-checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sci-checkbox"
|
||||
@change="itemSelected"
|
||||
/>
|
||||
<label :for="params.id" class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<div>{{ params.code }}</div>
|
||||
<RowMenuRenderer :params="{data: params, dtComponent: dtComponent}" class="ml-auto"/>
|
||||
</div>
|
||||
<a :href="params.urls.show" class="font-bold mb-4 text-sn-black hover:no-underline hover:text-sn-black">
|
||||
{{ params.name }}
|
||||
</a>
|
||||
<div class="grid gap-2 grid-cols-[80px_auto] mt-auto">
|
||||
<span class="text-sn-grey">{{ i18n.t('projects.index.card.start_date') }}</span>
|
||||
<span class="font-bold">{{ params.created_at }}</span>
|
||||
|
||||
<template v-if="params.archived_on">
|
||||
<span class="text-sn-grey">{{ i18n.t('projects.index.card.archived_date') }}</span>
|
||||
<span class="font-bold">{{ params.archived_on }}</span>
|
||||
</template>
|
||||
<span class="text-sn-grey">{{ i18n.t('projects.index.card.visibility') }}</span>
|
||||
<span class="font-bold">{{ params.hidden ? i18n.t('projects.index.hidden') : i18n.t('projects.index.visible') }}</span>
|
||||
|
||||
<span class="text-sn-grey">{{ i18n.t('projects.index.card.users') }}</span>
|
||||
<UsersRenderer :params="{data: params, value: params.users}" class="-mt-2.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-4 rounded sn-shadow-flyout flex flex-col">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="sci-checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sci-checkbox"
|
||||
@change="itemSelected"
|
||||
/>
|
||||
<label :for="params.id" class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<RowMenuRenderer :params="{data: params, dtComponent: dtComponent}" class="ml-auto"/>
|
||||
</div>
|
||||
<div class="flex-grow flex items-center justify-center min-h-[6rem] text-sn-blue">
|
||||
<i class="sn-icon sn-icon-folder"></i>
|
||||
</div>
|
||||
<a :href="params.urls.show" class="flex items-center justify-center gap-1 font-bold mb-2 text-sn-black hover:no-underline hover:text-sn-black">
|
||||
<i class="sn-icon mini sn-icon-mini-folder-left"></i>
|
||||
{{ params.name }}
|
||||
</a>
|
||||
<div class="flex items-center justify-center">
|
||||
{{ params.folder_info }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import RowMenuRenderer from '../shared/datatable/row_menu_renderer.vue'
|
||||
import UsersRenderer from './renderers/users.vue'
|
||||
import CardSelectorMixin from '../shared/datatable/mixins/card_selector.js'
|
||||
|
||||
export default {
|
||||
name: "ProjectCard",
|
||||
props: {
|
||||
params: Object,
|
||||
dtComponent: Object
|
||||
},
|
||||
components: {
|
||||
RowMenuRenderer,
|
||||
UsersRenderer
|
||||
},
|
||||
mixins: [CardSelectorMixin]
|
||||
}
|
||||
</script>
|
|
@ -1,19 +1,58 @@
|
|||
<template>
|
||||
<div class="h-full">
|
||||
<DataTable :columnDefs="columnDefs"
|
||||
tableId="ProjectsList"
|
||||
:dataUrl="dataSource"
|
||||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
:withRowMenu="true"
|
||||
:activePageUrl="activePageUrl"
|
||||
:archivedPageUrl="archivedPageUrl"
|
||||
:currentViewMode="currentViewMode"
|
||||
:filters="filters"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
/>
|
||||
</div>
|
||||
<DataTable :columnDefs="columnDefs"
|
||||
tableId="ProjectsList"
|
||||
:dataUrl="dataSource"
|
||||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
:withRowMenu="true"
|
||||
:activePageUrl="activePageUrl"
|
||||
:archivedPageUrl="archivedPageUrl"
|
||||
:currentViewMode="currentViewMode"
|
||||
:filters="filters"
|
||||
:viewRenders="viewRenders"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
@comments="openComments"
|
||||
@archive="archive"
|
||||
@restore="restore"
|
||||
@edit="edit"
|
||||
@create="create"
|
||||
@create_folder="createFolder"
|
||||
@delete_folders="deleteFolder"
|
||||
@export="exportProjects"
|
||||
@move="move"
|
||||
>
|
||||
<template #card="data">
|
||||
<ProjectCard :params="data.params" :dtComponent="data.dtComponent" ></ProjectCard>
|
||||
</template>
|
||||
</DataTable>
|
||||
<a href="#" ref="commentButton" class="open-comments-sidebar hidden" data-turbolinks="false" data-object-type="Project" data-object-id=""></a>
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('projects.index.archive_confirm_title')"
|
||||
:description="i18n.t('projects.index.archive_confirm')"
|
||||
:confirmClass="'btn btn-primary'"
|
||||
:confirmText="i18n.t('general.archive')"
|
||||
ref="archiveModal"
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('projects.index.modal_delete_folders.title')"
|
||||
:description="folderDeleteDescription"
|
||||
:confirmClass="'btn btn-danger'"
|
||||
:confirmText="i18n.t('projects.index.modal_delete_folders.confirm_button')"
|
||||
ref="deleteFolderModal"
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('projects.export_projects.modal_title')"
|
||||
:description="exportDescription"
|
||||
:confirmClass="'btn btn-primary'"
|
||||
:confirmText="i18n.t('projects.export_projects.export_button')"
|
||||
ref="exportModal"
|
||||
></ConfirmationModal>
|
||||
<EditProjectModal v-if="editProject" :userRolesUrl="userRolesUrl" :project="editProject" @close="editProject = null" @update="updateTable" />
|
||||
<EditFolderModal v-if="editFolder" :folder="editFolder" @close="editFolder = null" @update="updateTable" />
|
||||
<NewProjectModal v-if="newProject" :createUrl="createUrl" :currentFolderId="currentFolderId" :userRolesUrl="userRolesUrl" @close="newProject = false" @create="updateTable" />
|
||||
<NewFolderModal v-if="newFolder" :createFolderUrl="createFolderUrl" :currentFolderId="currentFolderId" :viewMode="currentViewMode" @close="newFolder = false" @create="updateTable" />
|
||||
<MoveModal v-if="objectsToMove" :moveToUrl="moveToUrl" :selectedObjects="objectsToMove" :foldersTreeUrl="foldersTreeUrl" @close="objectsToMove = null" @move="updateTable" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -21,42 +60,51 @@ import axios from '../../packs/custom_axios.js';
|
|||
|
||||
import DataTable from '../shared/datatable/table.vue'
|
||||
import UsersRenderer from './renderers/users.vue'
|
||||
import ProjectCard from './card.vue'
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue'
|
||||
import EditProjectModal from './modals/edit.vue'
|
||||
import EditFolderModal from './modals/edit_folder.vue'
|
||||
import NewProjectModal from './modals/new.vue'
|
||||
import NewFolderModal from './modals/new_folder.vue'
|
||||
import MoveModal from './modals/move.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProjectsList',
|
||||
components: {
|
||||
DataTable,
|
||||
UsersRenderer,
|
||||
ProjectCard,
|
||||
ConfirmationModal,
|
||||
EditProjectModal,
|
||||
EditFolderModal,
|
||||
NewProjectModal,
|
||||
NewFolderModal,
|
||||
MoveModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
type: String,
|
||||
},
|
||||
createFolderUrl: {
|
||||
type: String,
|
||||
},
|
||||
activePageUrl: {
|
||||
type: String,
|
||||
},
|
||||
archivedPageUrl: {
|
||||
type: String,
|
||||
},
|
||||
currentViewMode: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
dataSource: { type: String, required: true },
|
||||
actionsUrl: { type: String, required: true },
|
||||
createUrl: { type: String },
|
||||
createFolderUrl: { type: String },
|
||||
activePageUrl: { type: String },
|
||||
archivedPageUrl: { type: String },
|
||||
currentViewMode: { type: String, required: true },
|
||||
usersFilterUrl: { type: String },
|
||||
userRolesUrl: { type: String },
|
||||
currentFolderId: { type: String },
|
||||
foldersTreeUrl: { type: String },
|
||||
moveToUrl: { type: String },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newProject: false,
|
||||
newFolder: false,
|
||||
editProject: null,
|
||||
editFolder: null,
|
||||
objectsToMove: null,
|
||||
reloadingTable: false,
|
||||
folderDeleteDescription: '',
|
||||
exportDescription: '',
|
||||
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 },
|
||||
|
@ -67,9 +115,15 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
viewRenders() {
|
||||
return [
|
||||
{type: 'table'},
|
||||
{type: 'cards'}
|
||||
]
|
||||
},
|
||||
toolbarActions() {
|
||||
let left = []
|
||||
if (this.createUrl) {
|
||||
if (this.createUrl && this.currentViewMode !== 'archived') {
|
||||
left.push({
|
||||
name: 'create',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
|
@ -113,6 +167,16 @@ export default {
|
|||
})
|
||||
}
|
||||
|
||||
filters.push({
|
||||
key: 'members',
|
||||
type: 'Select',
|
||||
optionsUrl: this.usersFilterUrl,
|
||||
optionRenderer: this.usersFilterRenderer,
|
||||
labelRenderer: this.usersFilterRenderer,
|
||||
label: this.i18n.t("projects.index.filters_modal.members.label"),
|
||||
placeholder: this.i18n.t("projects.index.filters_modal.members.placeholder"),
|
||||
})
|
||||
|
||||
filters.push({
|
||||
key: 'folder_search',
|
||||
type: 'Checkbox',
|
||||
|
@ -123,6 +187,12 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
usersFilterRenderer(option) {
|
||||
return `<div class="flex items-center gap-2">
|
||||
<img src="${option[2].avatar_url}" class="rounded-full w-6 h-6" />
|
||||
<span>${option[1]}</span>
|
||||
</div>`
|
||||
},
|
||||
nameRenderer(params) {
|
||||
let showUrl = params.data.urls.show;
|
||||
return `<a href="${showUrl}" class="flex items-center gap-1">
|
||||
|
@ -132,9 +202,85 @@ export default {
|
|||
},
|
||||
visibiltyRenderer(params) {
|
||||
if (params.data.type !== 'projects') return ''
|
||||
|
||||
return params.data.hidden ? this.i18n.t('projects.index.hidden') : this.i18n.t('projects.index.visible');
|
||||
},
|
||||
openComments(_params, rows) {
|
||||
this.$refs.commentButton.dataset.objectId = rows[0].id;
|
||||
this.$refs.commentButton.click();
|
||||
},
|
||||
async archive(event, rows) {
|
||||
const ok = await this.$refs.archiveModal.show()
|
||||
if (ok) {
|
||||
axios.post(event.path, { project_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
restore(event, rows) {
|
||||
axios.post(event.path, { project_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
edit(event, rows) {
|
||||
if (rows[0].folder) {
|
||||
this.editFolder = rows[0]
|
||||
return
|
||||
}
|
||||
this.editProject = rows[0]
|
||||
},
|
||||
create() {
|
||||
this.newProject = true;
|
||||
},
|
||||
createFolder() {
|
||||
this.newFolder = true;
|
||||
},
|
||||
updateTable() {
|
||||
this.editProject = null;
|
||||
this.editFolder = null;
|
||||
this.newProject = false;
|
||||
this.newFolder = false;
|
||||
this.objectsToMove = null;
|
||||
this.reloadingTable = true;
|
||||
},
|
||||
async deleteFolder(event, rows) {
|
||||
const description =`
|
||||
<p>${this.i18n.t('projects.index.modal_delete_folders.description_1_html', {number: rows.length}) }</p>
|
||||
<p>${this.i18n.t('projects.index.modal_delete_folders.description_2')}</p>`
|
||||
this.folderDeleteDescription = description;
|
||||
const ok = await this.$refs.deleteFolderModal.show();
|
||||
if (ok) {
|
||||
axios.post(event.path, { project_folder_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
async exportProjects(event, rows) {
|
||||
this.exportDescription = event.message;
|
||||
const ok = await this.$refs.exportModal.show()
|
||||
if (ok) {
|
||||
axios.post(event.path, {
|
||||
project_ids: rows.filter((row) => !row.folder).map((row) => row.id),
|
||||
project_folder_ids: rows.filter((row) => row.folder).map((row) => row.id),
|
||||
}).then((response) => {
|
||||
this.reloadingTable = true
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
move(event, rows) {
|
||||
this.objectsToMove = rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
83
app/javascript/vue/projects/modals/edit.vue
Normal file
83
app/javascript/vue/projects/modals/edit.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="project.name">
|
||||
{{ i18n.t('projects.index.modal_edit_project.modal_title', {project: project.name}) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t("projects.index.modal_new_project.name") }}</label>
|
||||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs items-center">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
|
||||
</div>
|
||||
<div class="mt-6" :class="{'hidden': !visible}">
|
||||
<label class="sci-label">{{ i18n.t("user_assignment.select_default_user_role") }}</label>
|
||||
<SelectDropdown :optionsUrl="userRolesUrl" :value="defaultRole" @change="changeRole" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_edit_project.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectDropdown from "../../shared/select_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modal_mixin from "../../shared/modal_mixin";
|
||||
|
||||
export default {
|
||||
name: "EditProjectModal",
|
||||
props: {
|
||||
project: Object,
|
||||
userRolesUrl: String,
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.project.name,
|
||||
visible: this.project.visible,
|
||||
defaultRole: this.project.default_public_user_role_id,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.put(this.project.urls.update, {
|
||||
project: {
|
||||
name: this.name,
|
||||
visibility: (this.visible ? 'visible' : 'hidden'),
|
||||
default_public_user_role_id: this.defaultRole,
|
||||
}
|
||||
}).then(() => {
|
||||
this.error = null;
|
||||
this.$emit('update');
|
||||
}).catch((error) => {
|
||||
this.error = error.response.data.errors.name;
|
||||
})
|
||||
},
|
||||
changeRole(role) {
|
||||
this.defaultRole = role;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
64
app/javascript/vue/projects/modals/edit_folder.vue
Normal file
64
app/javascript/vue/projects/modals/edit_folder.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="folder.name">
|
||||
{{ i18n.t('projects.index.modal_edit_folder.title', {folder: folder.name}) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t("projects.index.modal_edit_folder.folder_name_field") }}</label>
|
||||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_edit_folder.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectDropdown from "../../shared/select_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modal_mixin from "../../shared/modal_mixin";
|
||||
|
||||
export default {
|
||||
name: "EditFolderModal",
|
||||
props: {
|
||||
folder: Object,
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.folder.name,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.put(this.folder.urls.update, {
|
||||
project_folder: {
|
||||
name: this.name,
|
||||
}
|
||||
}).then(() => {
|
||||
this.error = null;
|
||||
this.$emit('update');
|
||||
}).catch((error) => {
|
||||
this.error = error.response.data.errors.name;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
125
app/javascript/vue/projects/modals/move.vue
Normal file
125
app/javascript/vue/projects/modals/move.vue
Normal file
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
|
||||
{{ this.title }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4">{{ this.description }}</div>
|
||||
<div class="mb-4">
|
||||
<div class="sci-input-container-v2 left-icon">
|
||||
<input type="text" v-model="query" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_move_folder.find_folder')" />
|
||||
<i class="sn-icon sn-icon-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
<div class="p-2 flex items-center gap-2 cursor-pointer text-sn-blue hover:bg-sn-super-light-grey"
|
||||
@click="selectFolder(null)"
|
||||
:class="{'!bg-sn-super-light-blue': selectedFolderId == null}">
|
||||
<i class="sn-icon sn-icon-projects"></i>
|
||||
{{ i18n.t('projects.index.modal_move_folder.projects') }}
|
||||
</div>
|
||||
<MoveTree :objects="filteredFoldersTree" :value="selectedFolderId" @selectFolder="selectFolder" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_move_folder.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modal_mixin from "../../shared/modal_mixin";
|
||||
import MoveTree from './move_tree.vue';
|
||||
|
||||
export default {
|
||||
name: "NewProjectModal",
|
||||
props: {
|
||||
selectedObjects: Array,
|
||||
foldersTreeUrl: String,
|
||||
moveToUrl: String,
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
data() {
|
||||
return {
|
||||
selectedFolderId: null,
|
||||
foldersTree: [],
|
||||
query: '',
|
||||
};
|
||||
},
|
||||
components: {
|
||||
MoveTree
|
||||
},
|
||||
mounted() {
|
||||
axios.get(this.foldersTreeUrl).then((response) => {
|
||||
this.foldersTree = response.data;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
itemsName() {
|
||||
return this.i18n.t('projects.index.modal_move_folder.items.' + this.itemsType);
|
||||
},
|
||||
title() {
|
||||
return this.i18n.t('projects.index.modal_move_folder.title', {items: this.itemsName} );
|
||||
},
|
||||
description() {
|
||||
return this.i18n.t('projects.index.modal_move_folder.description', {items: this.itemsName} );
|
||||
},
|
||||
itemsType() {
|
||||
const allTypes = this.selectedObjects.map(obj => obj.type);
|
||||
const uniqueTypes = [...new Set(allTypes)];
|
||||
if (uniqueTypes.length == 1) {
|
||||
return uniqueTypes[0];
|
||||
} else {
|
||||
return 'projects_and_folders';
|
||||
}
|
||||
},
|
||||
filteredFoldersTree() {
|
||||
if (this.query == '') {
|
||||
return this.foldersTree;
|
||||
} else {
|
||||
return this.foldersTree.map((folder) => {
|
||||
return {
|
||||
folder: folder.folder,
|
||||
children: folder.children.filter((child) => {
|
||||
return child.folder.name.toLowerCase().includes(this.query.toLowerCase());
|
||||
})
|
||||
}
|
||||
}).filter((folder) => {
|
||||
return folder.folder.name.toLowerCase().includes(this.query.toLowerCase()) || folder.children.length > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectFolder(folderId) {
|
||||
this.selectedFolderId = folderId;
|
||||
},
|
||||
submit() {
|
||||
axios.post(this.moveToUrl, {
|
||||
destination_folder_id: this.selectedFolderId || 'root_folder',
|
||||
movables: this.selectedObjects.map((obj) => {
|
||||
return {
|
||||
id: obj.id,
|
||||
type: obj.type,
|
||||
}
|
||||
})
|
||||
}).then((response) => {
|
||||
this.$emit('move');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
37
app/javascript/vue/projects/modals/move_tree.vue
Normal file
37
app/javascript/vue/projects/modals/move_tree.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="pl-6" v-for="object in objects" :key="object.folder.id">
|
||||
<div class="flex items-center">
|
||||
<i v-if="object.children.length > 0"
|
||||
:class="{'sn-icon-up': opendedFolders[object.folder.id], 'sn-icon-down': !opendedFolders[object.folder.id]}"
|
||||
@click="opendedFolders[object.folder.id] = !opendedFolders[object.folder.id]"
|
||||
class="sn-icon p-2 pr-1 cursor-pointer"></i>
|
||||
<i v-else class="sn-icon sn-icon-up p-2 pr-1 opacity-0"></i>
|
||||
<div @click="$emit('selectFolder', object.folder.id)" class="cursor-pointer flex items-center pl-1 flex-1 gap-2 text-sn-blue hover:bg-sn-super-light-grey" :class="{'!bg-sn-super-light-blue': object.folder.id == value}">
|
||||
<i class="sn-icon sn-icon-folder"></i>
|
||||
<div class="flex-1 truncate p-2 pl-0" :title="object.folder.name">
|
||||
{{ object.folder.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MoveTree v-if="opendedFolders[object.folder.id]" :objects="object.children" :value="value" @selectFolder="$emit('selectFolder', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MoveTree',
|
||||
emits: ['selectFolder'],
|
||||
props: {
|
||||
objects: Array,
|
||||
value: Number,
|
||||
},
|
||||
components: {
|
||||
MoveTree: () => import('./move_tree.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
opendedFolders: {},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
85
app/javascript/vue/projects/modals/new.vue
Normal file
85
app/javascript/vue/projects/modals/new.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
|
||||
{{ i18n.t('projects.index.modal_new_project.modal_title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t("projects.index.modal_new_project.name") }}</label>
|
||||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs items-center">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
<span v-html="i18n.t('projects.index.modal_new_project.visibility_html')"></span>
|
||||
</div>
|
||||
<div class="mt-6" :class="{'hidden': !visible}">
|
||||
<label class="sci-label">{{ i18n.t("user_assignment.select_default_user_role") }}</label>
|
||||
<SelectDropdown :optionsUrl="userRolesUrl" :value="defaultRole" @change="changeRole" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_new_project.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectDropdown from "../../shared/select_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modal_mixin from "../../shared/modal_mixin";
|
||||
|
||||
export default {
|
||||
name: "NewProjectModal",
|
||||
props: {
|
||||
createUrl: String,
|
||||
userRolesUrl: String,
|
||||
currentFolderId: String,
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
visible: false,
|
||||
defaultRole: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.post(this.createUrl, {
|
||||
project: {
|
||||
name: this.name,
|
||||
visibility: (this.visible ? 'visible' : 'hidden'),
|
||||
default_public_user_role_id: this.defaultRole,
|
||||
project_folder_id: this.currentFolderId,
|
||||
}
|
||||
}).then(() => {
|
||||
this.error = null;
|
||||
this.$emit('create');
|
||||
}).catch((error) => {
|
||||
this.error = error.response.data.name;
|
||||
})
|
||||
},
|
||||
changeRole(role) {
|
||||
this.defaultRole = role;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
72
app/javascript/vue/projects/modals/new_folder.vue
Normal file
72
app/javascript/vue/projects/modals/new_folder.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
|
||||
{{ i18n.t('projects.index.modal_new_project_folder.modal_title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t('projects.index.modal_new_project_folder.name') }}</label>
|
||||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name" class="sci-input-field" autofocus="true" :placeholder="i18n.t('projects.index.modal_new_project.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">{{ i18n.t('projects.index.modal_new_project.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectDropdown from "../../shared/select_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modal_mixin from "../../shared/modal_mixin";
|
||||
|
||||
export default {
|
||||
name: "NewProjectModal",
|
||||
props: {
|
||||
createFolderUrl: String,
|
||||
userRolesUrl: String,
|
||||
viewMode: String,
|
||||
currentFolderId: String,
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.post(this.createFolderUrl, {
|
||||
project_folder: {
|
||||
name: this.name,
|
||||
parent_folder_id: this.currentFolderId,
|
||||
archived: (this.viewMode === 'archived')
|
||||
}
|
||||
}).then(() => {
|
||||
this.error = null;
|
||||
this.$emit('create');
|
||||
}).catch((error) => {
|
||||
this.error = error.response.data.name;
|
||||
})
|
||||
},
|
||||
changeRole(role) {
|
||||
this.defaultRole = role;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -35,7 +35,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SelectSearch from "../../shared/select_search.vue";
|
||||
import SelectSearch from "../../shared/legacy/select_search.vue";
|
||||
import repositoryValueMixin from "./mixins/repository_value.js";
|
||||
|
||||
export default {
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SelectSearch from "../../shared/select_search.vue";
|
||||
import SelectSearch from "../../shared/legacy/select_search.vue";
|
||||
import repositoryValueMixin from "./mixins/repository_value.js";
|
||||
import twemoji from "twemoji";
|
||||
|
||||
|
|
17
app/javascript/vue/shared/datatable/mixins/card_selector.js
Normal file
17
app/javascript/vue/shared/datatable/mixins/card_selector.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
methods: {
|
||||
itemSelected() {
|
||||
let item = this.dtComponent.selectedRows.find((item) => {
|
||||
return item.id == this.params.id;
|
||||
});
|
||||
|
||||
if (item) {
|
||||
this.dtComponent.selectedRows = this.dtComponent.selectedRows.filter((item) => {
|
||||
return item.id != this.params.id;
|
||||
});
|
||||
} else {
|
||||
this.dtComponent.selectedRows.push(this.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="items-center -ml-1.5 my-2">
|
||||
<div>
|
||||
<MenuDropdown
|
||||
:listItems="this.formattedList"
|
||||
btnClasses="bg-transparent w-6 h-6 border-0 p-0 flex"
|
||||
|
@ -7,6 +7,7 @@
|
|||
:alwaysShow="true"
|
||||
:btnIcon="'sn-icon sn-icon-more-hori'"
|
||||
@open="loadActions"
|
||||
@dtEvent="handleEvents"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,6 +42,8 @@ export default {
|
|||
newItem.url = item.path
|
||||
}
|
||||
|
||||
newItem.params = item
|
||||
|
||||
return newItem
|
||||
})
|
||||
}
|
||||
|
@ -53,9 +56,10 @@ export default {
|
|||
.then((response) => {
|
||||
this.actionsMenu = response.data.actions
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
handleEvents(event, option) {
|
||||
const dt = this.params.dtComponent
|
||||
dt.$emit(event, option.params, [this.params.data])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,20 @@
|
|||
:activePageUrl="activePageUrl"
|
||||
:archivedPageUrl="archivedPageUrl"
|
||||
:currentViewMode="currentViewMode"
|
||||
:currentViewRender="currentViewRender"
|
||||
:viewRenders="viewRenders"
|
||||
:filters="filters"
|
||||
@applyFilters="applyFilters"
|
||||
@setTableView="switchViewRender('table')"
|
||||
@setCardsView="switchViewRender('cards')"
|
||||
/>
|
||||
<div v-if="currentViewRender === 'cards'" class="flex-grow basis-64 overflow-y-auto overflow-x-visible p-2 -ml-2">
|
||||
<div class="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
<slot v-for="element in rowData" :key="element.id" name="card" :dtComponent="this" :params="element"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<ag-grid-vue
|
||||
v-if="currentViewRender === 'table'"
|
||||
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||
:class="{'opacity-0': initializing}"
|
||||
:columnDefs="columnDefs"
|
||||
|
@ -111,11 +121,13 @@ export default {
|
|||
type: String,
|
||||
default: 'active'
|
||||
},
|
||||
viewRenders: {
|
||||
type: Object,
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -132,7 +144,9 @@ export default {
|
|||
selectedRows: [],
|
||||
searchValue: '',
|
||||
initializing: true,
|
||||
activeFilters: {}
|
||||
activeFilters: {},
|
||||
currentViewRender: 'table',
|
||||
cardCheckboxes: []
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
@ -188,8 +202,11 @@ export default {
|
|||
resizable: false,
|
||||
sortable: false,
|
||||
cellRenderer: 'RowMenuRenderer',
|
||||
cellStyle: {overflow: 'visible'},
|
||||
pinned: 'right'
|
||||
cellRendererParams: {
|
||||
dtComponent: this
|
||||
},
|
||||
pinned: 'right',
|
||||
cellStyle: {padding: 0, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'visible'}
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -232,6 +249,7 @@ export default {
|
|||
.then((response) => {
|
||||
this.selectedRows = [];
|
||||
this.gridApi.setRowData(this.formatData(response.data.data));
|
||||
this.rowData = this.formatData(response.data.data);
|
||||
this.totalPage = response.data.meta.total_pages;
|
||||
this.$emit('tableReloaded');
|
||||
})
|
||||
|
@ -255,6 +273,7 @@ export default {
|
|||
},
|
||||
setPerPage(value) {
|
||||
this.perPage = value;
|
||||
this.page = 1;
|
||||
this.loadData();
|
||||
},
|
||||
setPage(page) {
|
||||
|
@ -296,6 +315,13 @@ export default {
|
|||
applyFilters(filters) {
|
||||
this.activeFilters = filters;
|
||||
this.loadData();
|
||||
},
|
||||
switchViewRender(view) {
|
||||
if (this.currentViewRender === view) return;
|
||||
|
||||
this.currentViewRender = view;
|
||||
this.initializing = true;
|
||||
this.selectedRows = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,14 +10,24 @@
|
|||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="archivedPageUrl" class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<MenuDropdown
|
||||
:listItems="this.viewModes"
|
||||
v-if="archivedPageUrl"
|
||||
:listItems="this.viewRendersMenu"
|
||||
:btnClasses="'btn btn-light icon-btn'"
|
||||
:btnText="i18n.t(`toolbar.${currentViewRender}_view`)"
|
||||
:caret="true"
|
||||
:position="'right'"
|
||||
@setCardsView="$emit('setCardsView')"
|
||||
@setTableView="$emit('setTableView')"
|
||||
></MenuDropdown>
|
||||
<MenuDropdown
|
||||
v-if="archivedPageUrl"
|
||||
:listItems="this.viewModesMenu"
|
||||
:btnClasses="'btn btn-light icon-btn'"
|
||||
:btnText="i18n.t(`projects.index.${currentViewMode}`)"
|
||||
:caret="true"
|
||||
:position="'right'"
|
||||
@open="loadActions"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,6 +84,14 @@ export default {
|
|||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
viewRenders: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
currentViewRender: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -81,11 +99,24 @@ export default {
|
|||
FilterDropdown
|
||||
},
|
||||
computed: {
|
||||
viewModes() {
|
||||
viewModesMenu() {
|
||||
return [
|
||||
{ text: this.i18n.t('projects.index.active'), url: this.activePageUrl},
|
||||
{ text: this.i18n.t('projects.index.archived'), url: this.archivedPageUrl }
|
||||
]
|
||||
},
|
||||
viewRendersMenu() {
|
||||
return this.viewRenders.map((view) => {
|
||||
const type = view.type;
|
||||
switch (type) {
|
||||
case 'cards':
|
||||
return { text: this.i18n.t('toolbar.cards_view'), emit: 'setCardsView'};
|
||||
case 'table':
|
||||
return { text: this.i18n.t('toolbar.table_view'), emit: 'setTableView'};
|
||||
default:
|
||||
return view;
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -1,15 +1,46 @@
|
|||
<template>
|
||||
<div class="mb-6">TODO</div>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ filter.label }}</label>
|
||||
<SelectDropdown
|
||||
:optionsUrl="filter.optionsUrl"
|
||||
:selectedValue="value"
|
||||
:multiple="true"
|
||||
:with-checkboxes="true"
|
||||
:placeholder="filter.placeholder"
|
||||
:optionRenderer="filter.optionRenderer"
|
||||
:labelRenderer="filter.labelRenderer"
|
||||
@change="change"
|
||||
> </SelectDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select from '../../legacy/select.vue';
|
||||
import SelectDropdown from '../../select_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'SelectFilter',
|
||||
props: {
|
||||
filter: { type: Object, required: true }
|
||||
},
|
||||
components: { Select }
|
||||
data: function() {
|
||||
return {
|
||||
value: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function() {
|
||||
let value = this.value;
|
||||
if (this.value.length == 0) {
|
||||
value = null;
|
||||
}
|
||||
this.$emit('update', { key: this.filter.key, value: value });
|
||||
}
|
||||
},
|
||||
components: { SelectDropdown },
|
||||
methods: {
|
||||
change: function(value) {
|
||||
this.value = value;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -114,7 +114,8 @@ export default {
|
|||
}
|
||||
|
||||
if (item.emit) {
|
||||
this.$emit(item.emit, item.params)
|
||||
this.$emit(item.emit, item.params);
|
||||
this.$emit('dtEvent', item.emit, item);
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
|
|
17
app/javascript/vue/shared/modal_mixin.js
Normal file
17
app/javascript/vue/shared/modal_mixin.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('close');
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
},
|
||||
}
|
|
@ -153,6 +153,13 @@ export default {
|
|||
return `${this.newValue.length} ${this.fewOptionsPlaceholder || this.i18n.t('general.select_dropdown.few_options_placeholder')}`
|
||||
}
|
||||
},
|
||||
valueChanged() {
|
||||
if (this.multiple) {
|
||||
return !this.compareArrays(this.newValue, this.value)
|
||||
} else {
|
||||
return this.newValue != this.value
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('scroll', this.setPosition);
|
||||
|
@ -205,8 +212,10 @@ export default {
|
|||
this.$emit('change', this.newValue)
|
||||
},
|
||||
close() {
|
||||
if (!this.isOpen) return;
|
||||
|
||||
this.isOpen = false
|
||||
if (this.newValue != this.value) {
|
||||
if (this.valueChanged) {
|
||||
this.$emit('change', this.newValue)
|
||||
}
|
||||
this.query = '';
|
||||
|
@ -274,6 +283,15 @@ export default {
|
|||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
compareArrays(arr1, arr2) {
|
||||
if (!arr1 || !arr2) return false;
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (!arr2.includes(arr1[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
<template>
|
||||
<Select
|
||||
class="sn-select sn-select--search"
|
||||
:className="className"
|
||||
:optionsClassName="optionsClassName"
|
||||
:withEditCursor="withEditCursor"
|
||||
:withClearButton="withClearButton"
|
||||
:value="value"
|
||||
:options="currentOptions"
|
||||
:placeholder="placeholder"
|
||||
:noOptionsPlaceholder="isLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
|
||||
v-bind:disabled="disabled"
|
||||
@change="change"
|
||||
@blur="blur"
|
||||
@open="open"
|
||||
@close="close"
|
||||
>
|
||||
<input ref="focusElement" v-model="query" type="text" class="sn-select__search-input" :placeholder="searchPlaceholder" />
|
||||
<span class="sn-select__value">{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
|
||||
<i class="sn-icon" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen}"></i>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select from './legacy/select.vue'
|
||||
|
||||
export default {
|
||||
name: 'SelectSearch',
|
||||
props: {
|
||||
withClearButton: { type: Boolean, default: false },
|
||||
withEditCursor: { type: Boolean, default: false },
|
||||
value: { type: [String, Number] },
|
||||
options: { type: Array, default: () => [] },
|
||||
optionsUrl: { type: String },
|
||||
placeholder: { type: String },
|
||||
searchPlaceholder: { type: String },
|
||||
noOptionsPlaceholder: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
className: { type: String, default: '' },
|
||||
optionsClassName: { type: String, default: '' }
|
||||
},
|
||||
components: { Select },
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
currentOptions: null,
|
||||
isOpen: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.currentOptions = this.options;
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
if(!this.query) {
|
||||
this.currentOptions = this.options;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.optionsUrl) {
|
||||
this.fetchOptions();
|
||||
} else {
|
||||
this.currentOptions = this.options.filter((o) => o[1].toLowerCase().includes(this.query.toLowerCase()));
|
||||
}
|
||||
},
|
||||
options() {
|
||||
this.currentOptions = this.options;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valueLabel() {
|
||||
let option = this.currentOptions.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
blur() {
|
||||
this.isOpen = false;
|
||||
this.$emit('blur');
|
||||
},
|
||||
change(value) {
|
||||
this.isOpen = false;
|
||||
this.$emit('change', value);
|
||||
},
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.$emit('open');
|
||||
},
|
||||
close() {
|
||||
this.query = '';
|
||||
this.isOpen = false;
|
||||
this.$emit('close');
|
||||
},
|
||||
fetchOptions() {
|
||||
$.get(`${this.optionsUrl}?query=${this.query || ''}`,
|
||||
(data) => {
|
||||
this.currentOptions = data;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -2,12 +2,16 @@ module Lists
|
|||
class ProjectAndFolderSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :name, :code, :created_at, :archived_on, :users, :hidden, :urls, :folder
|
||||
attributes :name, :code, :created_at, :archived_on, :users, :hidden, :urls, :folder, :folder_info, :default_public_user_role_id
|
||||
|
||||
def folder
|
||||
!project?
|
||||
end
|
||||
|
||||
def default_public_user_role_id
|
||||
object.default_public_user_role_id if project?
|
||||
end
|
||||
|
||||
def code
|
||||
object.code if project?
|
||||
end
|
||||
|
@ -36,11 +40,26 @@ module Lists
|
|||
end
|
||||
|
||||
def urls
|
||||
{
|
||||
urls_list = {
|
||||
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)
|
||||
}
|
||||
|
||||
if project?
|
||||
urls_list[:update] = project_path(object)
|
||||
else
|
||||
urls_list[:update] = project_folder_path(object)
|
||||
end
|
||||
|
||||
|
||||
urls_list
|
||||
end
|
||||
|
||||
def folder_info
|
||||
if folder
|
||||
I18n.t('projects.index.folder.description', projects_count: object.projects_count, folders_count: object.folders_count)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -80,8 +80,8 @@ module Lists
|
|||
end
|
||||
|
||||
def filter_project_folder_records(records)
|
||||
records = records.archived if @view_mode == 'archived'
|
||||
records = records.active if @view_mode == 'active'
|
||||
records = records.archived if @params[:view_mode] == 'archived'
|
||||
records = records.active if @params[:view_mode] == 'active'
|
||||
records = records.where_attributes_like('project_folders.name', @filters[:query]) if @filters[:query].present?
|
||||
records
|
||||
end
|
||||
|
|
|
@ -51,33 +51,21 @@ module Toolbars
|
|||
def edit_action
|
||||
return unless @single
|
||||
|
||||
action = {
|
||||
name: 'edit',
|
||||
label: I18n.t('projects.index.edit_option'),
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
button_class: 'edit-btn',
|
||||
type: :emit
|
||||
}
|
||||
|
||||
if @items.first.is_a?(Project)
|
||||
project = @items.first
|
||||
|
||||
return unless can_manage_project?(project)
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
label: I18n.t('projects.index.edit_option'),
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
button_class: 'edit-btn',
|
||||
path: edit_project_path(project),
|
||||
type: :emit
|
||||
}
|
||||
return unless can_manage_project?(@items.first)
|
||||
else
|
||||
project_folder = @items.first
|
||||
|
||||
return unless can_create_project_folders?(project_folder.team)
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
label: I18n.t('projects.index.edit_option'),
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
button_class: 'edit-btn',
|
||||
path: edit_project_folder_path(project_folder),
|
||||
type: :emit
|
||||
}
|
||||
return unless can_create_project_folders?(@items.first.team)
|
||||
end
|
||||
|
||||
action
|
||||
end
|
||||
|
||||
def access_action
|
||||
|
@ -120,11 +108,24 @@ module Toolbars
|
|||
def export_action
|
||||
return unless @items.all? { |item| item.is_a?(Project) ? can_export_project?(item) : true }
|
||||
|
||||
num_projects = @items.length
|
||||
limit = TeamZipExport.exports_limit
|
||||
num_of_requests_left = @current_user.exports_left - 1
|
||||
team = @items.first.team
|
||||
|
||||
message = "<p>#{I18n.t('projects.export_projects.modal_text_p1_html', num_projects: num_projects, team: team)}</p>
|
||||
<p>#{I18n.t('projects.export_projects.modal_text_p2_html')}</p>"
|
||||
unless limit.zero?
|
||||
message += "<p><i>#{I18n.t('projects.export_projects.modal_text_p3_html', limit: limit, num: num_of_requests_left)}</i></p>"
|
||||
end
|
||||
|
||||
{
|
||||
items: @items,
|
||||
name: 'export',
|
||||
label: I18n.t('projects.export_projects.export_button'),
|
||||
icon: 'sn-icon sn-icon-export',
|
||||
path: export_projects_modal_team_path(@items.first.team),
|
||||
message: message,
|
||||
path: export_projects_team_path(team),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
@ -167,7 +168,7 @@ module Toolbars
|
|||
name: 'delete_folders',
|
||||
label: I18n.t('general.delete'),
|
||||
icon: 'sn-icon sn-icon-delete',
|
||||
path: destroy_modal_project_folders_path(project_folder_ids: @items.map(&:id)),
|
||||
path: destroy_project_folders_path,
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
|
|
@ -7,74 +7,21 @@
|
|||
|
||||
<div id="ProjectsList" class="fixed-content-body">
|
||||
<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) %>"
|
||||
actions-url="<%= actions_toolbar_projects_path %>"
|
||||
users-filter-url="<%= users_filter_projects_path %>"
|
||||
data-source="<%= projects_path(project_folder_id: current_folder&.id, format: :json) %>"
|
||||
active-page-url="<%= projects_path(roject_folder_id: current_folder&.id, view_mode: :active) %>"
|
||||
archived-page-url="<%= projects_path(roject_folder_id: current_folder&.id, view_mode: :archived) %>"
|
||||
current-view-mode="<%= params[:view_mode] || :active %>"
|
||||
current-folder-id="<%= current_folder&.id %>"
|
||||
create-url="<%= projects_path if can_create_projects?(current_team) %>"
|
||||
create-folder-url="<%= project_folders_path if can_create_project_folders?(current_team) %>"
|
||||
user-roles-url="<%= user_roles_projects_path %>"
|
||||
folders-tree-url="<%= tree_project_folders_path(view_mode: params[:view_mode]) %>"
|
||||
move-to-url="<%= move_to_project_folders_path %>"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<%= render partial: 'projects/index/modals/edit_modal' %>
|
||||
<%= render partial: 'projects/index/modals/move_to_modal' %>
|
||||
<%= render partial: 'projects/index/modals/manage_users' %>
|
||||
<%= render partial: 'projects/index/modals/export_projects' %>
|
||||
|
||||
<div class="projects-container">
|
||||
<div class="cards-wrapper <%= cards_view_type_class(@current_view_type) %>"
|
||||
id="cardsWrapper"
|
||||
data-projects-cards-url="<%= @current_folder ? project_folder_cards_url(@current_folder) : cards_projects_url %>">
|
||||
<div class="table-header">
|
||||
<div class="table-header-cell select-all-checkboxes">
|
||||
<div class="sci-checkbox-container">
|
||||
<input value="1" type="checkbox" class="sci-checkbox select-all">
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-header-cell"><%= t('.card.name') %></div>
|
||||
<div class="table-header-cell"><%= t('.card.id') %></div>
|
||||
<div class="table-header-cell"><%= t('.card.start_date') %></div>
|
||||
<div class="table-header-cell" data-view-mode="archived"><%= t('.card.archived_date') %></div>
|
||||
<div class="table-header-cell"><%= t('.card.visibility') %></div>
|
||||
<div class="table-header-cell"><%= t('.card.users') %></div>
|
||||
<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>
|
||||
|
||||
<template id="projectPlaceholder">
|
||||
<div class="project-placeholder card">
|
||||
<% 4.times do |i| %>
|
||||
<div class="placeholder-element line-<%= i %>"></div>
|
||||
<% end %>
|
||||
<% 3.times do |i| %>
|
||||
<div class="placeholder-element circle circle-<%= i %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="projectEndOfList">
|
||||
<div class="project-list-end-placeholder">
|
||||
<i class="fas fa-flag-checkered"></i>
|
||||
<span><%= t('.end_of_list_placeholder') %></span>
|
||||
<i class="fas fa-flag-checkered"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<%= javascript_include_tag "vue_components_action_toolbar" %>
|
||||
<%= javascript_include_tag "projects/index" %>
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
<ul>
|
||||
<li class="jstree-open" id="root_folder" data-jstree='{"icon":"sn-icon sn-icon-projects root-folder"}'>
|
||||
<a class="jstree-clicked" href="#">Projects</a>
|
||||
<%= render partial: 'projects/index/modals/move_to_folders_tree',
|
||||
locals: { records: sidebar_folders_tree(current_team, current_user, @current_sort, folders_only: true) } %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<%= render partial: 'shared/sidebar/projects_tree_branch', locals: { records: sidebar_folders_tree(team, current_user, sort) } %>
|
||||
<% if !projects_view_mode_archived? %>
|
||||
<li class="sidebar-leaf">
|
||||
<%= link_to projects_path(view_mode: :archived), class: "sidebar-link" do %>
|
||||
|
|
|
@ -552,6 +552,7 @@ en:
|
|||
delete_button: "Delete"
|
||||
edit_option: "Edit"
|
||||
archive_option: "Archive"
|
||||
archive_confirm_title: "Archive project"
|
||||
archive_confirm: "Are you sure you want to archive this project?"
|
||||
restore_option: "Restore"
|
||||
project_members_access: "Access"
|
||||
|
@ -598,9 +599,11 @@ en:
|
|||
title: "Move %{items}"
|
||||
description: "Select where you want to move your %{items}"
|
||||
submit: "Move"
|
||||
find_folder: "Find folder"
|
||||
projects: "Projects"
|
||||
items:
|
||||
projects: 'projects'
|
||||
folders: 'folders'
|
||||
project_folders: 'folders'
|
||||
projects_and_folders: 'projects & folders'
|
||||
modal_delete_folders:
|
||||
title: "Delete project folder(s)"
|
||||
|
@ -3740,6 +3743,7 @@ en:
|
|||
selected: "Selected"
|
||||
loading: "Loading..."
|
||||
replace: "Replace"
|
||||
archive: "Archive"
|
||||
# In order to use the strings 'yes' and 'no' as keys, you need to wrap them with quotes
|
||||
'yes': "Yes"
|
||||
'no': "No"
|
||||
|
|
|
@ -369,6 +369,7 @@ Rails.application.routes.draw do
|
|||
post 'restore_group'
|
||||
put 'view_type', to: 'teams#view_type'
|
||||
get 'actions_toolbar'
|
||||
get :user_roles
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -376,6 +377,7 @@ Rails.application.routes.draw do
|
|||
get 'cards', to: 'projects#cards'
|
||||
|
||||
collection do
|
||||
get :tree
|
||||
post 'move_to', to: 'project_folders#move_to', defaults: { format: 'json' }
|
||||
get 'move_to_modal', to: 'project_folders#move_to_modal', defaults: { format: 'json' }
|
||||
post 'destroy', to: 'project_folders#destroy', as: 'destroy', defaults: { format: 'json' }
|
||||
|
|
Loading…
Reference in a new issue