mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-30 07:58:28 +08:00
Migrate access modal to VUE [SCI-9799]
This commit is contained in:
parent
5acec04794
commit
687bac024a
19 changed files with 763 additions and 282 deletions
|
|
@ -9,17 +9,59 @@ module AccessPermissions
|
|||
before_action :check_manage_permissions, except: %i(show)
|
||||
before_action :available_users, only: %i(new create)
|
||||
|
||||
def new
|
||||
@user_assignment = @project.user_assignments.new(
|
||||
assigned_by: current_user,
|
||||
team: current_team
|
||||
)
|
||||
def show
|
||||
render json: @project.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
|
||||
each_serializer: UserAssignmentSerializer
|
||||
end
|
||||
|
||||
def show; end
|
||||
def new
|
||||
render json: @available_users, each_serializer: UserSerializer
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
created_count = 0
|
||||
if permitted_create_params[:user_id] == 'all'
|
||||
@project.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id])
|
||||
log_activity(:project_grant_access_to_all_team_members,
|
||||
{ visibility: t('projects.activity.visibility_visible'),
|
||||
role: @project.default_public_user_role.name,
|
||||
team: @project.team.id })
|
||||
else
|
||||
user_assignment = UserAssignment.find_or_initialize_by(
|
||||
assignable: @project,
|
||||
user_id: permitted_create_params[:user_id],
|
||||
team: current_team
|
||||
)
|
||||
|
||||
user_assignment.update!(
|
||||
user_role_id: permitted_create_params[:user_role_id],
|
||||
assigned_by: current_user,
|
||||
assigned: :manually
|
||||
)
|
||||
|
||||
log_activity(:assign_user_to_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
created_count += 1
|
||||
propagate_job(user_assignment)
|
||||
end
|
||||
|
||||
@message = if created_count.zero?
|
||||
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
|
||||
else
|
||||
t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name))
|
||||
end
|
||||
render json: { message: @message }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error e.message
|
||||
errors = @project.errors.present? ? @project.errors&.map(&:message)&.join(',') : e.message
|
||||
render json: { flash: errors }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@user_assignment = @project.user_assignments.find_by(
|
||||
user_id: permitted_update_params[:user_id],
|
||||
|
|
@ -44,52 +86,6 @@ module AccessPermissions
|
|||
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
created_count = 0
|
||||
permitted_create_params[:resource_members].each do |_k, user_assignment_params|
|
||||
next unless user_assignment_params[:assign] == '1'
|
||||
|
||||
if user_assignment_params[:user_id] == 'all'
|
||||
@project.update!(visibility: :visible, default_public_user_role_id: user_assignment_params[:user_role_id])
|
||||
log_activity(:project_grant_access_to_all_team_members,
|
||||
{ visibility: t('projects.activity.visibility_visible'),
|
||||
role: @project.default_public_user_role.name,
|
||||
team: @project.team.id })
|
||||
else
|
||||
user_assignment = UserAssignment.find_or_initialize_by(
|
||||
assignable: @project,
|
||||
user_id: user_assignment_params[:user_id],
|
||||
team: current_team
|
||||
)
|
||||
|
||||
user_assignment.update!(
|
||||
user_role_id: user_assignment_params[:user_role_id],
|
||||
assigned_by: current_user,
|
||||
assigned: :manually
|
||||
)
|
||||
|
||||
log_activity(:assign_user_to_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
created_count += 1
|
||||
propagate_job(user_assignment)
|
||||
end
|
||||
end
|
||||
|
||||
@message = if created_count.zero?
|
||||
t('access_permissions.create.success', count: t('access_permissions.all_team'))
|
||||
else
|
||||
t('access_permissions.create.success', count: created_count)
|
||||
end
|
||||
render :edit
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error e.message
|
||||
errors = @project.errors.present? ? @project.errors&.map(&:message)&.join(',') : e.message
|
||||
render json: { flash: errors }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
user = @project.assigned_users.find(params[:user_id])
|
||||
user_assignment = @project.user_assignments.find_by(user: user, team: current_team)
|
||||
|
|
@ -100,12 +96,15 @@ module AccessPermissions
|
|||
end
|
||||
|
||||
propagate_job(user_assignment, destroy: true)
|
||||
|
||||
user_assignment.destroy!
|
||||
|
||||
log_activity(:unassign_user_from_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
|
||||
render json: { flash: t('access_permissions.destroy.success', member_name: escape_input(user.full_name)) }
|
||||
render json: { message: t('access_permissions.destroy.success', member_name: escape_input(user.full_name)) }
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render json: { flash: t('access_permissions.destroy.failure') },
|
||||
render json: { message: t('access_permissions.destroy.failure') },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ module AccessPermissions
|
|||
{ visibility: t('projects.activity.visibility_hidden'),
|
||||
role: previous_user_role_name,
|
||||
team: @project.team.id })
|
||||
render json: { flash: t('access_permissions.update.revoke_all_team_members') }
|
||||
render json: { message: t('access_permissions.update.revoke_all_team_members') }
|
||||
else
|
||||
# update all team members access
|
||||
@project.visibility = :visible
|
||||
|
|
@ -151,8 +150,8 @@ module AccessPermissions
|
|||
end
|
||||
|
||||
def permitted_create_params
|
||||
params.require(:access_permissions_new_user_form)
|
||||
.permit(resource_members: %i(assign user_id user_role_id))
|
||||
params.require(:user_assignment)
|
||||
.permit(%i(user_id user_role_id))
|
||||
end
|
||||
|
||||
def set_project
|
||||
|
|
@ -185,7 +184,7 @@ module AccessPermissions
|
|||
id: @project.user_assignments.automatically_assigned.select(:user_id)
|
||||
).or(
|
||||
current_team.users.where.not(id: @project.users.select(:id))
|
||||
)
|
||||
).order('users.full_name ASC')
|
||||
end
|
||||
|
||||
def log_activity(type_of, message_items = {})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ module AccessPermissions
|
|||
before_action :check_read_permissions, only: %i(show)
|
||||
before_action :check_manage_permissions, except: %i(show)
|
||||
|
||||
def show; end
|
||||
|
||||
def new
|
||||
@user_assignment = UserAssignment.new(
|
||||
assignable: @protocol,
|
||||
|
|
@ -14,32 +16,8 @@ module AccessPermissions
|
|||
)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
@user_assignment = @protocol.user_assignments.find_by(
|
||||
user_id: permitted_update_params[:user_id],
|
||||
team: current_team
|
||||
)
|
||||
|
||||
# prevent role change if it would result in no manually assigned users having the user management permission
|
||||
new_user_role = UserRole.find(permitted_update_params[:user_role_id])
|
||||
if !new_user_role.has_permission?(ProtocolPermissions::USERS_MANAGE) &&
|
||||
@user_assignment.last_with_permission?(ProtocolPermissions::USERS_MANAGE, assigned: :manually)
|
||||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
@user_assignment.update!(permitted_update_params)
|
||||
log_activity(:protocol_template_access_changed, { user_target: @user_assignment.user.id,
|
||||
role: @user_assignment.user_role.name })
|
||||
|
||||
render :protocol_member
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
created_count = 0
|
||||
|
|
@ -83,6 +61,28 @@ module AccessPermissions
|
|||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@user_assignment = @protocol.user_assignments.find_by(
|
||||
user_id: permitted_update_params[:user_id],
|
||||
team: current_team
|
||||
)
|
||||
|
||||
# prevent role change if it would result in no manually assigned users having the user management permission
|
||||
new_user_role = UserRole.find(permitted_update_params[:user_role_id])
|
||||
if !new_user_role.has_permission?(ProtocolPermissions::USERS_MANAGE) &&
|
||||
@user_assignment.last_with_permission?(ProtocolPermissions::USERS_MANAGE, assigned: :manually)
|
||||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
@user_assignment.update!(permitted_update_params)
|
||||
log_activity(:protocol_template_access_changed, { user_target: @user_assignment.user.id,
|
||||
role: @user_assignment.user_role.name })
|
||||
|
||||
render :protocol_member
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render json: { flash: t('access_permissions.update.failure') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
user = @protocol.assigned_users.find(params[:user_id])
|
||||
user_assignment = @protocol.user_assignments.find_by(user: user, team: current_team)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<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">
|
||||
<a :href="params.urls.show" :class="{'pointer-events-none text-sn-grey': !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">
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<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" />
|
||||
<UsersRenderer :params="{data: params, value: params.users, dtComponent: dtComponent}" class="-mt-2.5" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="p-4 rounded sn-shadow-flyout flex flex-col">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
@delete_folders="deleteFolder"
|
||||
@export="exportProjects"
|
||||
@move="move"
|
||||
@access="access"
|
||||
>
|
||||
<template #card="data">
|
||||
<ProjectCard :params="data.params" :dtComponent="data.dtComponent" ></ProjectCard>
|
||||
|
|
@ -37,14 +38,14 @@
|
|||
<ConfirmationModal
|
||||
:title="i18n.t('projects.index.modal_delete_folders.title')"
|
||||
:description="folderDeleteDescription"
|
||||
:confirmClass="'btn btn-danger'"
|
||||
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'"
|
||||
confirmClass="btn btn-primary"
|
||||
:confirmText="i18n.t('projects.export_projects.export_button')"
|
||||
ref="exportModal"
|
||||
></ConfirmationModal>
|
||||
|
|
@ -53,6 +54,7 @@
|
|||
<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" />
|
||||
<AccessModal v-if="accessModalParams" :params="accessModalParams" @close="accessModalParams = null" @refresh="this.reloadingTable = true" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -67,6 +69,7 @@ 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'
|
||||
import AccessModal from '../shared/access_modal/modal.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProjectsList',
|
||||
|
|
@ -79,7 +82,8 @@ export default {
|
|||
EditFolderModal,
|
||||
NewProjectModal,
|
||||
NewFolderModal,
|
||||
MoveModal
|
||||
MoveModal,
|
||||
AccessModal
|
||||
},
|
||||
props: {
|
||||
dataSource: { type: String, required: true },
|
||||
|
|
@ -97,6 +101,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
accessModalParams: null,
|
||||
newProject: false,
|
||||
newFolder: false,
|
||||
editProject: null,
|
||||
|
|
@ -110,7 +115,7 @@ export default {
|
|||
{ 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 }
|
||||
{ field: "users", headerName: this.i18n.t('projects.index.card.users'), cellRenderer: 'UsersRenderer', sortable: false, minWidth: 210, notSelectable: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -195,7 +200,7 @@ export default {
|
|||
},
|
||||
nameRenderer(params) {
|
||||
let showUrl = params.data.urls.show;
|
||||
return `<a href="${showUrl}" class="flex items-center gap-1">
|
||||
return `<a href="${showUrl}" class="flex items-center gap-1 hover:no-underline ${!showUrl ? 'pointer-events-none text-sn-grey' : ''}">
|
||||
${params.data.folder ? '<i class="sn-icon mini sn-icon-mini-folder-left"></i>' : ''}
|
||||
${params.data.name}
|
||||
</a>`
|
||||
|
|
@ -208,6 +213,12 @@ export default {
|
|||
this.$refs.commentButton.dataset.objectId = rows[0].id;
|
||||
this.$refs.commentButton.click();
|
||||
},
|
||||
access(event, rows) {
|
||||
this.accessModalParams = {
|
||||
object: rows[0],
|
||||
roles_path: this.userRolesUrl,
|
||||
}
|
||||
},
|
||||
async archive(event, rows) {
|
||||
const ok = await this.$refs.archiveModal.show()
|
||||
if (ok) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="!params.data.folder" class="flex items-center gap-1 cursor pointer h-10">
|
||||
<div v-if="!params.data.folder" class="flex items-center gap-1 cursor-pointer h-10" @click="openAccessModal">
|
||||
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
|
||||
<img :src="user.avatar" class="w-7 h-7" />
|
||||
</div>
|
||||
|
|
@ -18,7 +18,7 @@ export default {
|
|||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
|
|
@ -33,6 +33,11 @@ export default {
|
|||
hiddenUsersTitle() {
|
||||
return this.hiddenUsers.map((user) => user.full_name).join("\u000d")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openAccessModal() {
|
||||
this.params.dtComponent.$emit('access', {} ,[this.params.data]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
178
app/javascript/vue/shared/access_modal/edit.vue
Normal file
178
app/javascript/vue/shared/access_modal/edit.vue
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<div>
|
||||
<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">
|
||||
{{ i18n.t(`access_permissions.${params.object.type}.modals.edit_modal.title`, {resource_name: params.object.name}) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="h-[60vh] overflow-y-auto">
|
||||
<div v-for="userAssignment in manuallyAssignedUsers" :key="userAssignment.id" class="p-2 flex items-center gap-2">
|
||||
<div>
|
||||
<img :src="userAssignment.attributes.user.avatar_url" class="rounded-full w-8 h-8">
|
||||
</div>
|
||||
<div>{{ userAssignment.attributes.user.name }}</div>
|
||||
<MenuDropdown
|
||||
v-if="!userAssignment.attributes.last_owner"
|
||||
class="ml-auto"
|
||||
:listItems="rolesFromatted"
|
||||
:btnText="userAssignment.attributes.user_role.name"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
@setRole="(...args) => this.changeRole(userAssignment.attributes.user.id, ...args)"
|
||||
@removeRole="() => this.removeRole(userAssignment.attributes.user.id)"
|
||||
></MenuDropdown>
|
||||
<div class="ml-auto btn btn-light pointer-events-none" v-else>
|
||||
{{ userAssignment.attributes.user_role.name }}
|
||||
<div class="h-6 w-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="roles.length > 0 && visible" class="p-2 flex items-center gap-2">
|
||||
<div>
|
||||
<img src="/images/icon/team.png" class="rounded-full w-8 h-8">
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.t('access_permissions.everyone_else', { team_name: params.object.team }) }}
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-info" :title='this.autoAssignedUsers.map((ua) => ua.attributes.user.name).join("\u000d")'></i>
|
||||
<MenuDropdown
|
||||
class="ml-auto"
|
||||
:listItems="rolesFromatted"
|
||||
:btnText="this.roles.find((role) => role[0] == default_role)[1]"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
@setRole="(...args) => this.changeDefaultRole(...args)"
|
||||
@removeRole="() => this.changeDefaultRole()"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="params.object.top_level_assignable" class="modal-footer">
|
||||
<button class="btn-light ml-auto btn" @click="$emit('changeMode', 'newView')">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
{{ i18n.t('access_permissions.grant_access') }}
|
||||
</button >
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuDropdown from "../../shared/menu_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean
|
||||
},
|
||||
default_role: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
emits: ['changeMode', 'modified'],
|
||||
mounted() {
|
||||
this.getAssignedUsers();
|
||||
this.getRoles();
|
||||
},
|
||||
components: {
|
||||
MenuDropdown,
|
||||
},
|
||||
computed: {
|
||||
rolesFromatted() {
|
||||
let roles = this.roles.map((role) => {
|
||||
return {
|
||||
emit: 'setRole',
|
||||
text: role[1],
|
||||
params: role[0]
|
||||
}
|
||||
});
|
||||
|
||||
roles.push({
|
||||
dividerBefore: true,
|
||||
emit: 'removeRole',
|
||||
text: this.i18n.t('access_permissions.remove_access'),
|
||||
});
|
||||
|
||||
return roles
|
||||
},
|
||||
manuallyAssignedUsers() {
|
||||
return this.assignedUsers.filter((user) => {
|
||||
return user.attributes.assigned === 'manually';
|
||||
});
|
||||
},
|
||||
autoAssignedUsers() {
|
||||
return this.assignedUsers.filter((user) => {
|
||||
return user.attributes.assigned === 'automatically';
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
assignedUsers: [],
|
||||
roles: [],
|
||||
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getAssignedUsers() {
|
||||
axios.get(this.params.object.urls.show_access)
|
||||
.then((response) => {
|
||||
this.assignedUsers = response.data.data;
|
||||
})
|
||||
},
|
||||
getRoles() {
|
||||
axios.get(this.params.roles_path)
|
||||
.then((response) => {
|
||||
this.roles = response.data.data;
|
||||
})
|
||||
},
|
||||
changeRole(id, role_id) {
|
||||
axios.put(this.params.object.urls.show_access, {
|
||||
user_assignment: {
|
||||
user_id: id,
|
||||
user_role_id: role_id
|
||||
}
|
||||
}).then((response) => {
|
||||
this.$emit('modified');
|
||||
this.getAssignedUsers();
|
||||
})
|
||||
},
|
||||
removeRole(id) {
|
||||
axios.delete(this.params.object.urls.show_access, {
|
||||
data: {
|
||||
user_id: id
|
||||
}
|
||||
}).then((response) => {
|
||||
this.$emit('modified');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
this.getAssignedUsers();
|
||||
})
|
||||
},
|
||||
changeDefaultRole(role_id) {
|
||||
axios.put(this.params.object.urls.default_public_user_role_path, {
|
||||
project: {
|
||||
default_public_user_role_id: role_id || ''
|
||||
}
|
||||
}).then((response) => {
|
||||
this.$emit('modified');
|
||||
if (!role_id) {
|
||||
this.$emit('changeVisibility', false, null);
|
||||
} else {
|
||||
this.$emit('changeVisibility', true, role_id);
|
||||
}
|
||||
if (response.data.message) {
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}
|
||||
})
|
||||
},
|
||||
removeDefaultRole() {
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
68
app/javascript/vue/shared/access_modal/modal.vue
Normal file
68
app/javascript/vue/shared/access_modal/modal.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<component
|
||||
:is="mode"
|
||||
:params="params"
|
||||
:visible="visible"
|
||||
:default_role="default_role"
|
||||
@changeMode="changeMode"
|
||||
@modified="modified = true"
|
||||
@changeVisibility="changeVisibility"
|
||||
></component>
|
||||
</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";
|
||||
import editView from './edit.vue';
|
||||
import newView from './new.vue';
|
||||
|
||||
export default {
|
||||
name: "AccessModal",
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
mixins: [modal_mixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
editView,
|
||||
newView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mode: 'editView',
|
||||
modified: false,
|
||||
visible: false,
|
||||
default_role: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.visible = !this.params.object.hidden;
|
||||
this.default_role = this.params.object.default_public_user_role_id;
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.modified) {
|
||||
this.$emit('refresh');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeMode(mode) {
|
||||
this.mode = mode;
|
||||
},
|
||||
changeVisibility(status, role) {
|
||||
this.visible = status;
|
||||
this.default_role = role;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
136
app/javascript/vue/shared/access_modal/new.vue
Normal file
136
app/javascript/vue/shared/access_modal/new.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div>
|
||||
<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 flex items-center gap-4">
|
||||
{{ i18n.t('access_permissions.partials.new_assignments_form.title', {resource_name: params.object.name}) }}
|
||||
<button class="close" @click="$emit('changeMode', 'editView')">
|
||||
<i class="sn-icon sn-icon-left"></i>
|
||||
</button>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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('access_permissions.partials.new_assignments_form.find_people_html')" />
|
||||
<i class="sn-icon sn-icon-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[60vh] overflow-y-auto">
|
||||
<div v-if="!visible && roles.length > 0" class="p-2 flex items-center gap-2">
|
||||
<div>
|
||||
<img src="/images/icon/team.png" class="rounded-full w-8 h-8">
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.t('user_assignment.assign_all_team_members') }}
|
||||
</div>
|
||||
<MenuDropdown
|
||||
class="ml-auto"
|
||||
:listItems="rolesFromatted"
|
||||
btnText="Assign"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
@setRole="(...args) => this.assignRole('all', ...args)"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
<div v-for="user in filteredUsers" :key="user.id" class="p-2 flex items-center gap-2">
|
||||
<div>
|
||||
<img :src="user.attributes.avatar_url" class="rounded-full w-8 h-8">
|
||||
</div>
|
||||
<div>{{ user.attributes.name }}</div>
|
||||
<MenuDropdown
|
||||
class="ml-auto"
|
||||
:listItems="rolesFromatted"
|
||||
btnText="Assign"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
@setRole="(...args) => this.assignRole(user.id, ...args)"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuDropdown from "../../shared/menu_dropdown.vue";
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean
|
||||
},
|
||||
default_role: {
|
||||
type: Number
|
||||
},
|
||||
},
|
||||
emits: ['changeMode'],
|
||||
mounted() {
|
||||
this.getUnAssignedUsers();
|
||||
this.getRoles();
|
||||
},
|
||||
components: {
|
||||
MenuDropdown,
|
||||
},
|
||||
computed: {
|
||||
rolesFromatted() {
|
||||
return this.roles.map((role) => {
|
||||
return {
|
||||
emit: 'setRole',
|
||||
text: role[1],
|
||||
params: role[0]
|
||||
}
|
||||
});
|
||||
},
|
||||
filteredUsers() {
|
||||
return this.unAssignedUsers.filter((user) => {
|
||||
return user.attributes.name.toLowerCase().includes(this.query.toLowerCase());
|
||||
});
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
unAssignedUsers: [],
|
||||
roles: [],
|
||||
query: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getUnAssignedUsers() {
|
||||
axios.get(this.params.object.urls.new_access)
|
||||
.then((response) => {
|
||||
this.unAssignedUsers = response.data.data;
|
||||
})
|
||||
},
|
||||
getRoles() {
|
||||
axios.get(this.params.roles_path)
|
||||
.then((response) => {
|
||||
this.roles = response.data.data;
|
||||
})
|
||||
},
|
||||
assignRole(id, role_id) {
|
||||
axios.post(this.params.object.urls.create_access, {
|
||||
user_assignment: {
|
||||
user_id: id,
|
||||
user_role_id: role_id
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
this.$emit('modified');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
this.getUnAssignedUsers();
|
||||
|
||||
if (id === 'all') {
|
||||
this.$emit('changeVisibility', true, role_id);
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
v-if="currentViewRender === 'table'"
|
||||
class="ag-theme-alpine w-full flex-grow h-full z-10"
|
||||
:class="{'opacity-0': initializing}"
|
||||
:columnDefs="columnDefs"
|
||||
:columnDefs="extendedColumnDefs"
|
||||
:rowData="rowData"
|
||||
:defaultColDef="defaultColDef"
|
||||
:rowSelection="'multiple'"
|
||||
|
|
@ -177,38 +177,48 @@ export default {
|
|||
return {
|
||||
suppressCellFocus: true
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.withCheckboxes) {
|
||||
this.columnDefs.unshift({
|
||||
field: "checkbox",
|
||||
headerCheckboxSelection: true,
|
||||
headerCheckboxSelectionFilteredOnly: true,
|
||||
checkboxSelection: true,
|
||||
width: 48,
|
||||
minWidth: 48,
|
||||
resizable: false,
|
||||
pinned: 'left'
|
||||
},
|
||||
extendedColumnDefs() {
|
||||
let columns = this.columnDefs.map(column => {
|
||||
return {
|
||||
...column,
|
||||
cellRendererParams: {
|
||||
dtComponent: this
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.withRowMenu) {
|
||||
this.columnDefs.push({
|
||||
field: "rowMenu",
|
||||
headerName: '',
|
||||
width: 42,
|
||||
minWidth: 42,
|
||||
resizable: false,
|
||||
sortable: false,
|
||||
cellRenderer: 'RowMenuRenderer',
|
||||
cellRendererParams: {
|
||||
dtComponent: this
|
||||
},
|
||||
pinned: 'right',
|
||||
cellStyle: {padding: 0, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'visible'}
|
||||
if (this.withCheckboxes) {
|
||||
columns.unshift({
|
||||
field: "checkbox",
|
||||
headerCheckboxSelection: true,
|
||||
headerCheckboxSelectionFilteredOnly: true,
|
||||
checkboxSelection: true,
|
||||
width: 48,
|
||||
minWidth: 48,
|
||||
resizable: false,
|
||||
pinned: 'left'
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
if (this.withRowMenu) {
|
||||
columns.push({
|
||||
field: "rowMenu",
|
||||
headerName: '',
|
||||
width: 42,
|
||||
minWidth: 42,
|
||||
resizable: false,
|
||||
sortable: false,
|
||||
cellRenderer: 'RowMenuRenderer',
|
||||
cellRendererParams: {
|
||||
dtComponent: this
|
||||
},
|
||||
pinned: 'right',
|
||||
cellStyle: {padding: 0, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'visible'}
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -308,7 +318,7 @@ export default {
|
|||
this.loadData();
|
||||
},
|
||||
clickCell(e) {
|
||||
if (e.column.colId !== 'rowMenu') {
|
||||
if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) {
|
||||
e.node.setSelected(true);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,69 +1,69 @@
|
|||
<template>
|
||||
<div class="relative" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
|
||||
<button ref="openBtn" :class="btnClasses" @click="showMenu = !showMenu">
|
||||
<button ref="field" :class="btnClasses" @click="isOpen = !isOpen">
|
||||
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||
{{ btnText }}
|
||||
<i v-if="caret && showMenu" class="sn-icon sn-icon-up"></i>
|
||||
<i v-if="caret && isOpen" class="sn-icon sn-icon-up"></i>
|
||||
<i v-else-if="caret" class="sn-icon sn-icon-down"></i>
|
||||
</button>
|
||||
<div ref="flyout"
|
||||
class="absolute z-[150] bg-sn-white rounded p-2.5 sn-shadow-menu-sm min-w-full flex flex-col gap-[1px]"
|
||||
:class="{
|
||||
'right-0': position === 'right',
|
||||
'left-0': position === 'left',
|
||||
'bottom-0': openUp,
|
||||
'!mb-0': !openUp,
|
||||
}"
|
||||
v-if="showMenu"
|
||||
>
|
||||
<span v-for="(item, i) in listItems" :key="i" class="contents">
|
||||
<div v-if="item.dividerBefore" class="border-0 border-t border-solid border-sn-light-grey"></div>
|
||||
<a :href="item.url" v-if="!item.submenu"
|
||||
:target="item.url_target || '_self'"
|
||||
:class="{ 'bg-sn-super-light-blue': item.active }"
|
||||
:data-toggle="item.modalTarget && 'modal'"
|
||||
: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 leading-5"
|
||||
@click="handleClick($event, item)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
<div v-else class="-mx-2.5 px-2.5 group relative">
|
||||
<span
|
||||
<teleport to="body">
|
||||
<div ref="flyout"
|
||||
class="fixed z-[3000] bg-sn-white inline-block rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
|
||||
:class="{
|
||||
'right-0': position === 'right',
|
||||
'left-0': position === 'left',
|
||||
}"
|
||||
v-if="isOpen"
|
||||
>
|
||||
<span v-for="(item, i) in listItems" :key="i" class="contents">
|
||||
<div v-if="item.dividerBefore" class="border-0 border-t border-solid border-sn-light-grey"></div>
|
||||
<a :href="item.url" v-if="!item.submenu"
|
||||
:target="item.url_target || '_self'"
|
||||
:class="{ 'bg-sn-super-light-blue': item.active }"
|
||||
class="flex group items-center rounded relative text-sn-blue whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer group-hover:bg-sn-super-light-blue hover:!bg-sn-super-light-grey"
|
||||
:data-toggle="item.modalTarget && 'modal'"
|
||||
: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 leading-5"
|
||||
@click="handleClick($event, item)"
|
||||
>
|
||||
{{ item.text }}
|
||||
<i class="sn-icon sn-icon-right ml-auto"></i>
|
||||
</span>
|
||||
<div
|
||||
class="absolute bg-sn-white rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px] tw-hidden group-hover:block"
|
||||
:class="{
|
||||
'left-0 ml-[100%]': item.position === 'right',
|
||||
'right-0 mr-[100%]': item.position === 'left',
|
||||
'bottom-0': openUp,
|
||||
'top-0': !openUp,
|
||||
}"
|
||||
>
|
||||
<a v-for="(sub_item, si) in item.submenu" :key="si"
|
||||
:href="sub_item.url"
|
||||
:traget="sub_item.url_target || '_self'"
|
||||
</a>
|
||||
<div v-else class="-mx-2.5 px-2.5 group relative">
|
||||
<span
|
||||
: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 leading-5"
|
||||
@click="handleClick($event, sub_item)"
|
||||
class="flex group items-center rounded relative text-sn-blue whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer group-hover:bg-sn-super-light-blue hover:!bg-sn-super-light-grey"
|
||||
>
|
||||
{{ sub_item.text }}
|
||||
</a>
|
||||
{{ item.text }}
|
||||
<i class="sn-icon sn-icon-right ml-auto"></i>
|
||||
</span>
|
||||
<div
|
||||
class="absolute bg-sn-white rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px] tw-hidden group-hover:block"
|
||||
:class="{
|
||||
'left-0 ml-[100%]': item.position === 'right',
|
||||
'right-0 mr-[100%]': item.position === 'left',
|
||||
'bottom-0': openUp,
|
||||
'top-0': !openUp,
|
||||
}"
|
||||
>
|
||||
<a v-for="(sub_item, si) in item.submenu" :key="si"
|
||||
:href="sub_item.url"
|
||||
:traget="sub_item.url_target || '_self'"
|
||||
: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 leading-5"
|
||||
@click="handleClick($event, sub_item)"
|
||||
>
|
||||
{{ sub_item.text }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import isInViewPort from './isInViewPort.js';
|
||||
import FixedFlyoutMixin from './mixins/fixed_flyout.js';
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
|
||||
export default {
|
||||
|
|
@ -79,34 +79,26 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false,
|
||||
openUp: false
|
||||
isOpen: false
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
'click-outside': vOnClickOutside
|
||||
},
|
||||
mixins: [FixedFlyoutMixin],
|
||||
watch: {
|
||||
showMenu() {
|
||||
if (this.showMenu) {
|
||||
this.openUp = false;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.flyout.style.marginBottom = `${this.$refs.openBtn.offsetHeight}px`;
|
||||
this.updateOpenDirectoin();
|
||||
})
|
||||
isOpen() {
|
||||
if (this.isOpen) {
|
||||
this.$emit('open');
|
||||
this.$nextTick(() => {
|
||||
this.setPosition();
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('scroll', this.updateOpenDirectoin);
|
||||
},
|
||||
unmounted() {
|
||||
document.removeEventListener('scroll', this.updateOpenDirectoin);
|
||||
},
|
||||
methods: {
|
||||
closeMenu() {
|
||||
this.showMenu = false;
|
||||
this.isOpen = false;
|
||||
},
|
||||
handleClick(event, item) {
|
||||
if (!item.url) {
|
||||
|
|
@ -117,16 +109,7 @@ export default {
|
|||
this.$emit(item.emit, item.params);
|
||||
this.$emit('dtEvent', item.emit, item);
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
updateOpenDirectoin() {
|
||||
if (!this.showMenu) return;
|
||||
|
||||
this.openUp = false;
|
||||
this.$nextTick(() => {
|
||||
this.openUp = !isInViewPort(this.$refs.flyout);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
app/javascript/vue/shared/mixins/fixed_flyout.js
Normal file
71
app/javascript/vue/shared/mixins/fixed_flyout.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
overflowContainerScrollTop: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isOpen() {
|
||||
if (this.isOpen) {
|
||||
this.$nextTick(() => {
|
||||
this.overflowContainerListener();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
overflowContainerListener() {
|
||||
const { field, flyout } = this.$refs;
|
||||
if (!field || !flyout) return;
|
||||
|
||||
let fieldRect = field.getBoundingClientRect();
|
||||
|
||||
if (this.overflowContainerScrollTop !== fieldRect.top) {
|
||||
this.setPosition();
|
||||
}
|
||||
this.overflowContainerScrollTop = fieldRect.top;
|
||||
|
||||
setTimeout(() => {
|
||||
this.overflowContainerListener();
|
||||
}, 10);
|
||||
},
|
||||
setPosition() {
|
||||
const { field, flyout } = this.$refs;
|
||||
|
||||
if (!field || !flyout) return;
|
||||
|
||||
const rect = field.getBoundingClientRect();
|
||||
const screenHeight = window.innerHeight;
|
||||
|
||||
const windowHasScroll = document.documentElement.scrollHeight > document.documentElement.clientHeight;
|
||||
let rightScrollOffset = 0;
|
||||
const { left, width } = rect;
|
||||
const top = rect.top + rect.height;
|
||||
const bottom = screenHeight - rect.bottom + rect.height;
|
||||
const right = window.innerWidth - rect.right;
|
||||
if (windowHasScroll) {
|
||||
rightScrollOffset = 14;
|
||||
}
|
||||
|
||||
if (this.fixedWidth) {
|
||||
flyout.style.width = `${width}px`;
|
||||
} else {
|
||||
flyout.style.minWidth = `${width}px`;
|
||||
}
|
||||
if (this.position === 'right') {
|
||||
flyout.style.right = `${right - rightScrollOffset}px`;
|
||||
} else {
|
||||
flyout.style.left = `${left}px`;
|
||||
}
|
||||
if (bottom < top) {
|
||||
flyout.style.bottom = `${bottom}px`;
|
||||
flyout.style.top = 'unset';
|
||||
flyout.style.boxShadow = '0px -16px 32px 0px rgba(16, 24, 40, 0.07)';
|
||||
} else {
|
||||
flyout.style.top = `${top}px`;
|
||||
flyout.style.bottom = 'unset';
|
||||
flyout.style.boxShadow = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -23,43 +23,46 @@
|
|||
<i v-if="canClear" @click="clear" class="sn-icon ml-auto sn-icon-close"></i>
|
||||
<i v-else class="sn-icon ml-auto" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
|
||||
</div>
|
||||
<div v-if="isOpen" ref="flyout" class="bg-white sn-shadow-menu-sm rounded w-full fixed z-50">
|
||||
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
|
||||
<div @click="selectAll" :class="sizeClass" class="border-x-0 border-transparent border-solid border-b-sn-light-grey py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
|
||||
<div class="sn-checkbox-icon"
|
||||
:class="selectAllState"
|
||||
></div>
|
||||
{{ i18n.t('general.select_all') }}
|
||||
</div>
|
||||
</div>
|
||||
<perfect-scrollbar class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
|
||||
<template v-for="option in filteredOptions" :key="option[0]">
|
||||
<div
|
||||
@click="setValue(option[0])"
|
||||
class="py-1.5 px-3 rounded cursor-pointer flex items-center gap-2 shrink-0"
|
||||
:class="[sizeClass, {'!bg-sn-super-light-blue': valueSelected(option[0])}]"
|
||||
>
|
||||
<div v-if="withCheckboxes"
|
||||
class="sn-checkbox-icon"
|
||||
:class="{
|
||||
'checked': valueSelected(option[0]),
|
||||
'unchecked': !valueSelected(option[0]),
|
||||
}"
|
||||
<teleport to="body">
|
||||
<div v-if="isOpen" ref="flyout" class="bg-white inline-block sn-shadow-menu-sm rounded w-full fixed z-[3000]">
|
||||
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
|
||||
<div @click="selectAll" :class="sizeClass" class="border-x-0 border-transparent border-solid border-b-sn-light-grey py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
|
||||
<div class="sn-checkbox-icon"
|
||||
:class="selectAllState"
|
||||
></div>
|
||||
<div v-if="optionRenderer" v-html="optionRenderer(option)"></div>
|
||||
<div v-else >{{ option[1] }}</div>
|
||||
{{ i18n.t('general.select_all') }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="filteredOptions.length === 0" class="text-sn-grey text-center py-2.5">
|
||||
{{ noOptionsPlaceholder || this.i18n.t('general.select_dropdown.no_options_placeholder') }}
|
||||
</div>
|
||||
</perfect-scrollbar>
|
||||
</div>
|
||||
<perfect-scrollbar class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
|
||||
<template v-for="option in filteredOptions" :key="option[0]">
|
||||
<div
|
||||
@click="setValue(option[0])"
|
||||
class="py-1.5 px-3 rounded cursor-pointer flex items-center gap-2 shrink-0"
|
||||
:class="[sizeClass, {'!bg-sn-super-light-blue': valueSelected(option[0])}]"
|
||||
>
|
||||
<div v-if="withCheckboxes"
|
||||
class="sn-checkbox-icon"
|
||||
:class="{
|
||||
'checked': valueSelected(option[0]),
|
||||
'unchecked': !valueSelected(option[0]),
|
||||
}"
|
||||
></div>
|
||||
<div v-if="optionRenderer" v-html="optionRenderer(option)"></div>
|
||||
<div v-else >{{ option[1] }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="filteredOptions.length === 0" class="text-sn-grey text-center py-2.5">
|
||||
{{ noOptionsPlaceholder || this.i18n.t('general.select_dropdown.no_options_placeholder') }}
|
||||
</div>
|
||||
</perfect-scrollbar>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FixedFlyoutMixin from './mixins/fixed_flyout.js';
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
|
||||
export default {
|
||||
|
|
@ -91,8 +94,10 @@ export default {
|
|||
fetchedOptions: [],
|
||||
selectAllState: 'unchecked',
|
||||
query: '',
|
||||
fixedWidth: true
|
||||
}
|
||||
},
|
||||
mixins: [FixedFlyoutMixin],
|
||||
computed: {
|
||||
sizeClass() {
|
||||
switch (this.size) {
|
||||
|
|
@ -162,16 +167,12 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('scroll', this.setPosition);
|
||||
this.newValue = this.value;
|
||||
if (!this.newValue && this.multiple) {
|
||||
this.newValue = []
|
||||
}
|
||||
this.fetchOptions();
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('scroll', this.setPosition);
|
||||
},
|
||||
watch: {
|
||||
isOpen() {
|
||||
if (this.isOpen) {
|
||||
|
|
@ -240,38 +241,6 @@ export default {
|
|||
}
|
||||
this.$emit('change', this.newValue)
|
||||
},
|
||||
setPosition() {
|
||||
const field= this.$refs.field;
|
||||
const flyout = this.$refs.flyout;
|
||||
const rect = field.getBoundingClientRect();
|
||||
const screenHeight = window.innerHeight;
|
||||
|
||||
if (!this.isOpen) return;
|
||||
|
||||
let width = rect.width;
|
||||
let height = rect.height;
|
||||
let top = rect.top + rect.height;
|
||||
let bottom = screenHeight - rect.bottom + rect.height;
|
||||
let left = rect.left;
|
||||
|
||||
const modal = field.closest('.modal-content');
|
||||
if (modal) {
|
||||
const modalRect = modal.getBoundingClientRect();
|
||||
top -= modalRect.top;
|
||||
left -= modalRect.left;
|
||||
}
|
||||
|
||||
flyout.style.width = `${width}px`;
|
||||
flyout.style.top = `${top}px`;
|
||||
flyout.style.left = `${left}px`;
|
||||
if (bottom < top) {
|
||||
flyout.style.marginTop = `${(height + flyout.offsetHeight)* -1}px`;
|
||||
flyout.style.boxShadow = '0px -16px 32px 0px rgba(16, 24, 40, 0.07)';
|
||||
} else {
|
||||
flyout.style.marginTop = '';
|
||||
flyout.style.boxShadow = '';
|
||||
}
|
||||
},
|
||||
fetchOptions() {
|
||||
if (this.optionsUrl) {
|
||||
fetch(`${this.optionsUrl}?query=${this.query || ''}`)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ module Lists
|
|||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :name, :language_type, :urls, :type,
|
||||
:default, :format, :modified_by, :created_by,
|
||||
:created_at, :updated_at, :id, :icon_url, :description
|
||||
:default, :format, :modified_by, :created_by,
|
||||
:created_at, :updated_at, :id, :icon_url, :description
|
||||
|
||||
def icon_url
|
||||
ActionController::Base.helpers.image_tag(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
module Lists
|
||||
class ProjectAndFolderSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
|
||||
attributes :name, :code, :created_at, :archived_on, :users, :hidden, :urls, :folder, :folder_info, :default_public_user_role_id
|
||||
attributes :name, :code, :created_at, :archived_on, :users, :hidden, :urls, :folder,
|
||||
:folder_info, :default_public_user_role_id, :team, :top_level_assignable
|
||||
|
||||
def team
|
||||
object.team.name
|
||||
end
|
||||
|
||||
def folder
|
||||
!project?
|
||||
end
|
||||
|
||||
def top_level_assignable
|
||||
project?
|
||||
end
|
||||
|
||||
def default_public_user_role_id
|
||||
object.default_public_user_role_id if project?
|
||||
end
|
||||
|
|
@ -46,19 +56,29 @@ module Lists
|
|||
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[:show] = nil if project? && !can_read_project?(object)
|
||||
|
||||
urls_list[:update] = if project?
|
||||
project_path(object)
|
||||
else
|
||||
project_folder_path(object)
|
||||
end
|
||||
|
||||
if project? && can_manage_project_users?(object)
|
||||
urls_list[:show_access] = access_permissions_project_path(object)
|
||||
urls_list[:new_access] = new_access_permissions_project_path(id: object.id)
|
||||
urls_list[:create_access] = access_permissions_projects_path(id: object.id)
|
||||
urls_list[:default_public_user_role_path] =
|
||||
update_default_public_user_role_access_permissions_project_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)
|
||||
I18n.t('projects.index.folder.description', projects_count: object.projects_count,
|
||||
folders_count: object.folders_count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
27
app/serializers/user_assignment_serializer.rb
Normal file
27
app/serializers/user_assignment_serializer.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserAssignmentSerializer < ActiveModel::Serializer
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :id, :assigned, :assignable_type, :user, :user_role, :last_owner
|
||||
|
||||
def user
|
||||
{
|
||||
id: object.user.id,
|
||||
name: object.user.name,
|
||||
avatar_url: avatar_path(object, :icon_small)
|
||||
}
|
||||
end
|
||||
|
||||
def user_role
|
||||
{
|
||||
id: object.user_role.id,
|
||||
name: object.user_role.name
|
||||
}
|
||||
end
|
||||
|
||||
def last_owner
|
||||
object.last_with_permission?("#{object.assignable.class.name}Permissions".constantize::USERS_MANAGE, assigned: :manually)
|
||||
end
|
||||
end
|
||||
12
app/serializers/user_serializer.rb
Normal file
12
app/serializers/user_serializer.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserSerializer < ActiveModel::Serializer
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :id, :name, :avatar_url
|
||||
|
||||
def avatar_url
|
||||
avatar_path(object, :icon_small)
|
||||
end
|
||||
end
|
||||
|
|
@ -77,17 +77,9 @@ module Toolbars
|
|||
|
||||
return unless can_manage_team?(project.team) || can_read_project?(project)
|
||||
|
||||
path = if can_manage_project_users?(project)
|
||||
edit_access_permissions_project_path(project)
|
||||
else
|
||||
access_permissions_project_path(project)
|
||||
end
|
||||
|
||||
{
|
||||
name: 'access',
|
||||
label: I18n.t('general.access'),
|
||||
icon: 'sn-icon sn-icon-project-member-access',
|
||||
path: path,
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3591,7 +3591,7 @@ en:
|
|||
remove_access: "Remove access"
|
||||
grant_access: "Grant new access"
|
||||
create:
|
||||
success: "You have successfully granted access to %{count} member(s)."
|
||||
success: "You have successfully granted access to %{member_name}."
|
||||
failure: "Something went wrong"
|
||||
destroy:
|
||||
success: "You have successfully removed %{member_name}."
|
||||
|
|
@ -3621,9 +3621,9 @@ en:
|
|||
my_module_member_field:
|
||||
reset: "Inherit role"
|
||||
reset_description: "The inherited role from project or experiment will be applied"
|
||||
project: "Project"
|
||||
project_tooltip: "This role was set on this project."
|
||||
project_tooltip_inherit: "This role was inherited from the project."
|
||||
projects: "Project"
|
||||
projects_tooltip: "This role was set on this project."
|
||||
projects_tooltip_inherit: "This role was inherited from the project."
|
||||
protocol: "Protocol"
|
||||
protocol_tooltip: "This role was set on this protocol."
|
||||
protocol_tooltip_inherit: "This role was inherited from the protocol."
|
||||
|
|
|
|||
BIN
public/images/icon/team.png
Normal file
BIN
public/images/icon/team.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 KiB |
Loading…
Add table
Reference in a new issue