mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-07 05:34:55 +08:00
Add user groups table [SCI-11955]
This commit is contained in:
parent
1184767a62
commit
935b1f5cd8
18 changed files with 722 additions and 14 deletions
|
@ -17,6 +17,10 @@
|
|||
.fixed-content-body {
|
||||
height: calc(100vh - var(--content-header-size) - var(--navbar-height));
|
||||
width: 100%;
|
||||
|
||||
&.user-groups-table-container {
|
||||
height: calc(100vh - var(--content-header-size) - var(--navbar-height) - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
|
|
|
@ -11,7 +11,21 @@ module Users
|
|||
|
||||
def show; end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
new_users = @team.users.where(id: params[:user_ids])
|
||||
|
||||
new_users.each do |user|
|
||||
@user_group.user_group_memberships.create!(user: user, created_by: current_user)
|
||||
end
|
||||
|
||||
render json: { message: :success }, status: :created
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error e.message
|
||||
head :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
def destroy; end
|
||||
|
||||
|
@ -25,12 +39,12 @@ module Users
|
|||
@user_group = @team.user_groups.find(params[:user_group_id])
|
||||
end
|
||||
|
||||
def load_user_group
|
||||
def load_user_group_membership
|
||||
@user_group_membership = @user_group.user_group_memberships.find(params[:id])
|
||||
end
|
||||
|
||||
def check_manage_permissions
|
||||
render_403 unless can_manage_team(@team)
|
||||
render_403 unless can_manage_team?(@team)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,22 +4,75 @@ module Users
|
|||
module Settings
|
||||
class UserGroupsController < ApplicationController
|
||||
before_action :load_team
|
||||
before_action :load_user_group, except: :index
|
||||
before_action :check_manage_permissions, except: %i(index show)
|
||||
before_action :set_breadcrumbs_items
|
||||
before_action :load_user_group, except: %i(index unassigned_users actions_toolbar create)
|
||||
before_action :check_manage_permissions, except: %i(index show unassigned_users actions_toolbar)
|
||||
before_action :set_breadcrumbs_items, only: %i(index show)
|
||||
|
||||
def index
|
||||
@active_tab = :user_groups
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@active_tab = :user_groups
|
||||
end
|
||||
format.json do
|
||||
user_groups = Lists::UserGroupsService.new(@team.user_groups, params).call
|
||||
render json: user_groups, each_serializer: Lists::UserGroupSerializer, user: current_user, meta: pagination_dict(user_groups)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions:
|
||||
Toolbars::UserGroupsService.new(
|
||||
current_user,
|
||||
@team,
|
||||
user_group_ids: JSON.parse(params[:items]).pluck('id')
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
||||
def unassigned_users
|
||||
@unassigned_users = @team.users.search(false, params[:query])
|
||||
if params[:user_group_id].present?
|
||||
@user_group = @team.user_groups.find(params[:user_group_id])
|
||||
@unassigned_users = @unassigned_users.where.not(id: @user_group.users.select(:id))
|
||||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
@user_group = @team.user_groups.new
|
||||
@user_group.created_by = current_user
|
||||
@user_group.last_modified_by = current_user
|
||||
@user_group.assign_attributes(user_group_params)
|
||||
|
||||
def destroy; end
|
||||
if @user_group.save
|
||||
render json: { message: t('user_groups.create.success') }, status: :created
|
||||
else
|
||||
render json: { errors: t('user_groups.create.error') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update; end
|
||||
|
||||
def destroy
|
||||
if @user_group.destroy
|
||||
render json: { message: t('user_groups.delete.success') }, status: :ok
|
||||
else
|
||||
render json: { errors: t('user_groups.delete.error') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_group_params
|
||||
params.require(:user_group).permit(
|
||||
:name,
|
||||
user_group_memberships_attributes: %i(id user_id)
|
||||
)
|
||||
end
|
||||
|
||||
def load_team
|
||||
@team = Team.find(params[:team_id])
|
||||
end
|
||||
|
@ -29,7 +82,7 @@ module Users
|
|||
end
|
||||
|
||||
def check_manage_permissions
|
||||
render_403 unless can_manage_team(@team)
|
||||
render_403 unless can_manage_team?(@team)
|
||||
end
|
||||
|
||||
def set_breadcrumbs_items
|
||||
|
|
10
app/javascript/packs/vue/user_groups_table.js
Normal file
10
app/javascript/packs/vue/user_groups_table.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import UserGroupsTable from '../../vue/user_groups/index.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp();
|
||||
app.component('UserGroupsTable', UserGroupsTable);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
app.use(PerfectScrollbar);
|
||||
mountWithTurbolinks(app, '#userGroupsTable');
|
140
app/javascript/vue/user_groups/index.vue
Normal file
140
app/javascript/vue/user_groups/index.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div class="h-full">
|
||||
<DataTable :columnDefs="columnDefs"
|
||||
:tableId="'UserGroups'"
|
||||
:dataUrl="dataSource"
|
||||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
@assignUsers="assignUsers"
|
||||
@create="newGroup = true"
|
||||
@delete="deleteGroup"
|
||||
/>
|
||||
</div>
|
||||
<CreateModal v-if="newGroup" :createUrl="createUrl" :usersUrl="usersUrl"
|
||||
@close="newGroup = false" @create="reloadingTable = true; newGroup = false" />
|
||||
<DeleteModal
|
||||
:title="i18n.t('user_groups.index.delete_modal.title')"
|
||||
:description="deleteDescription"
|
||||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('user_groups.index.delete_modal.confirm')"
|
||||
ref="deleteModal"
|
||||
></DeleteModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import MembersRenderer from './renderers/members.vue';
|
||||
import CreateModal from './modal/create_group.vue';
|
||||
import DeleteModal from '../shared/confirmation_modal.vue';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'UserGroupsTable',
|
||||
components: {
|
||||
DataTable,
|
||||
NameRenderer,
|
||||
MembersRenderer,
|
||||
CreateModal,
|
||||
DeleteModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
type: String
|
||||
},
|
||||
usersUrl: {
|
||||
type: String
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reloadingTable: false,
|
||||
newGroup: false,
|
||||
deleteDescription: '',
|
||||
columnDefs: [
|
||||
{
|
||||
field: 'name',
|
||||
headerName: this.i18n.t('user_groups.index.group_name'),
|
||||
sortable: true,
|
||||
cellRenderer: 'NameRenderer'
|
||||
}, {
|
||||
field: 'members',
|
||||
headerName: this.i18n.t('user_groups.index.members'),
|
||||
sortable: true,
|
||||
cellRenderer: 'MembersRenderer',
|
||||
minWidth: 210,
|
||||
notSelectable: true
|
||||
}, {
|
||||
field: 'created_by',
|
||||
headerName: this.i18n.t('user_groups.index.created_by'),
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'created_at',
|
||||
headerName: this.i18n.t('user_groups.index.created_on'),
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'updated_at',
|
||||
headerName: this.i18n.t('user_groups.index.updated_on'),
|
||||
sortable: true,
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toolbarActions() {
|
||||
const left = [];
|
||||
if (this.createUrl) {
|
||||
left.push({
|
||||
name: 'create',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('user_groups.index.new_group'),
|
||||
type: 'emit',
|
||||
path: this.createUrl,
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
}
|
||||
return {
|
||||
left,
|
||||
right: []
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
assignUsers(params, selectedUsers) {
|
||||
axios.post(params.data.urls.assign_users, { user_ids: selectedUsers })
|
||||
.then(() => {
|
||||
this.reloadingTable = true;
|
||||
})
|
||||
},
|
||||
async deleteGroup(event, rows) {
|
||||
const sanitizedName = rows[0].name.replace(/<\/?[^>]+(>|$)/g, "")
|
||||
const description = this.i18n.t('user_groups.index.delete_modal.description_html', { group: sanitizedName });
|
||||
this.deleteDescription = description;
|
||||
const ok = await this.$refs.deleteModal.show();
|
||||
if (ok) {
|
||||
axios.delete(event.path).then((response) => {
|
||||
this.reloadingTable = true;
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
137
app/javascript/vue/user_groups/modal/create_group.vue
Normal file
137
app/javascript/vue/user_groups/modal/create_group.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<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">
|
||||
{{ i18n.t('user_groups.index.create_modal.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-4">
|
||||
{{ i18n.t('user_groups.index.create_modal.description') }}
|
||||
</p>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t('user_groups.index.create_modal.name') }}</label>
|
||||
<div class="sci-input-container-v2">
|
||||
<input type="text" v-model="name" class="sci-input-field"
|
||||
autofocus="true" ref="input"
|
||||
:placeholder="i18n.t('user_groups.index.create_modal.name_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<label class="sci-label">{{ i18n.t('user_groups.index.create_modal.select_members') }}</label>
|
||||
<SelectDropdown
|
||||
:optionsUrl="usersUrl"
|
||||
@change="changeUsers"
|
||||
:withCheckboxes="true"
|
||||
:option-renderer="usersRenderer"
|
||||
:label-renderer="usersRenderer"
|
||||
:multiple="true"
|
||||
:placeholder="i18n.t('user_groups.index.create_modal.select_members_placeholder')"
|
||||
/>
|
||||
</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"
|
||||
type="submit"
|
||||
:disabled="submitting || !validName"
|
||||
>
|
||||
{{ i18n.t('user_groups.index.create_modal.create_button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectDropdown from '../../shared/select_dropdown.vue';
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'ProjectFormModal',
|
||||
props: {
|
||||
usersUrl: String,
|
||||
createUrl: String
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
components: {
|
||||
SelectDropdown,
|
||||
},
|
||||
computed: {
|
||||
validName() {
|
||||
return this.name.length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH;
|
||||
},
|
||||
modalHeader() {
|
||||
if (this.createUrl) {
|
||||
return this.i18n.t('projects.index.modal_new_project.modal_title');
|
||||
}
|
||||
|
||||
return this.i18n.t('projects.index.modal_edit_project.modal_title', { project: this.project?.name });
|
||||
},
|
||||
submitButtonLabel() {
|
||||
if (this.createUrl) {
|
||||
return this.i18n.t('projects.index.modal_new_project.create');
|
||||
}
|
||||
|
||||
return this.i18n.t('projects.index.modal_edit_project.submit');
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: this.project?.name || '',
|
||||
users: [],
|
||||
submitting: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeUsers(selectedUsers) {
|
||||
this.users = selectedUsers;
|
||||
},
|
||||
async submit() {
|
||||
this.submitting = true;
|
||||
|
||||
const userGroupData = {
|
||||
name: this.name,
|
||||
user_group_memberships_attributes: this.users.map((user) => ({
|
||||
user_id: user
|
||||
}))
|
||||
};
|
||||
|
||||
await axios.post(this.createUrl, {
|
||||
user_group: userGroupData
|
||||
}).then((data) => {
|
||||
HelperModule.flashAlertMsg(data.data.message, 'success');
|
||||
this.$emit('create');
|
||||
}).catch((data) => {
|
||||
console.log(data);
|
||||
HelperModule.flashAlertMsg(data.response.data.error, 'danger');
|
||||
});
|
||||
this.submitting = false;
|
||||
},
|
||||
usersRenderer(user) {
|
||||
return `<div class="flex items-center gap-2 truncate">
|
||||
<img class="w-6 h-6 rounded-full" src="${user[2].avatar}">
|
||||
<span title="${user[1]}" class="truncate">${user[1]}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
128
app/javascript/vue/user_groups/renderers/members.vue
Normal file
128
app/javascript/vue/user_groups/renderers/members.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="params.data.team_users_count > users.length && params.data.permissions.manage">
|
||||
<GeneralDropdown @open="loadUsers" @close="closeFlyout" position="right">
|
||||
<template v-slot:field>
|
||||
<div class="flex items-center gap-1 cursor-pointer h-9">
|
||||
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
|
||||
<img :src="user.avatar" class="w-7 h-7 rounded-full" />
|
||||
</div>
|
||||
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle"
|
||||
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
|
||||
+{{ hiddenUsers.length }}
|
||||
</div>
|
||||
<div class="flex items-center shrink-0 justify-center w-7 h-7 rounded-full border-dashed bg-sn-white text-sn-sleepy-grey border-sn-sleepy-grey">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:flyout>
|
||||
<div class="px-2">
|
||||
<div class="sci-input-container-v2 left-icon mb-1 -mx-2.5">
|
||||
<input type="text"
|
||||
v-model="query"
|
||||
class="sci-input-field"
|
||||
autofocus="true"
|
||||
:placeholder="i18n.t('general.search')" />
|
||||
<i class="sn-icon sn-icon-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col relative max-h-80 overflow-y-auto max-w-[280px] pt-1 -mx-2.5 px-2 gap-y-px">
|
||||
<div v-for="user in unassginedUsers"
|
||||
:key="user.value"
|
||||
@click="selectUser(user)"
|
||||
:class="{
|
||||
'!bg-sn-super-light-blue': selectedUsers.some(({ value }) => value === user[0])
|
||||
}"
|
||||
class="whitespace-nowrap rounded py-2.5 flex items-center gap-2
|
||||
hover:no-underline leading-5 cursor-pointer hover:bg-sn-super-light-grey px-3"
|
||||
>
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" :checked="selectedUsers.some(({ value }) => value === user[0])" />
|
||||
<label class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<img :src="user[2].avatar" class="w-6 h-6 rounded-full" />
|
||||
<span class="truncate">{{ user[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex items-center gap-1 cursor-pointer h-9">
|
||||
<div v-for="(user, i) in visibleUsers" :key="i" :title="user.full_name">
|
||||
<img :src="user.avatar" class="w-7 h-7 rounded-full" />
|
||||
</div>
|
||||
<div v-if="hiddenUsers.length > 0" :title="hiddenUsersTitle"
|
||||
class="flex shrink-0 items-center justify-center w-7 h-7 text-xs rounded-full bg-sn-dark-grey text-sn-white">
|
||||
+{{ hiddenUsers.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import GeneralDropdown from '../../shared/general_dropdown.vue';
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: 'MembersRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
GeneralDropdown
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
unassginedUsers: [],
|
||||
selectedUsers: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
this.loadUsers();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
return this.params.value || [];
|
||||
},
|
||||
visibleUsers() {
|
||||
return this.users.slice(0, 4);
|
||||
},
|
||||
hiddenUsers() {
|
||||
return this.users.slice(4);
|
||||
},
|
||||
hiddenUsersTitle() {
|
||||
return this.hiddenUsers.map((user) => user.full_name).join('\u000d');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectUser(user) {
|
||||
const index = this.selectedUsers.findIndex(({ value }) => value === user[0]);
|
||||
if (index > -1) {
|
||||
this.selectedUsers.splice(index, 1);
|
||||
} else {
|
||||
this.selectedUsers.push(user[0]);
|
||||
}
|
||||
},
|
||||
loadUsers() {
|
||||
axios.get(this.params.data.urls.unassigned_users, { params: { query: this.query } })
|
||||
.then((response) => {
|
||||
this.unassginedUsers = response.data.data
|
||||
})
|
||||
},
|
||||
closeFlyout() {
|
||||
if (this.selectedUsers.length > 0) {
|
||||
this.params.dtComponent.$emit('assignUsers', this.params, this.selectedUsers);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
16
app/javascript/vue/user_groups/renderers/name.vue
Normal file
16
app/javascript/vue/user_groups/renderers/name.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<a :href="params.data.urls.show" :title="params.data.name">
|
||||
{{ params.data.name }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserGroup < ApplicationRecord
|
||||
include SearchableModel
|
||||
|
||||
validates :name,
|
||||
presence: true,
|
||||
length: { minimum: Constants::NAME_MIN_LENGTH,
|
||||
|
@ -12,4 +14,13 @@ class UserGroup < ApplicationRecord
|
|||
belongs_to :last_modified_by, class_name: 'User', optional: true
|
||||
has_many :user_group_memberships, dependent: :destroy
|
||||
has_many :users, through: :user_group_memberships, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :user_group_memberships
|
||||
|
||||
def user_group_memberships_attributes=(attributes)
|
||||
attributes.each do |membership|
|
||||
membership[:created_by_id] = last_modified_by_id
|
||||
user_group_memberships.build(membership)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@ class UserGroupMembership < ApplicationRecord
|
|||
belongs_to :user_group
|
||||
belongs_to :user
|
||||
belongs_to :created_by, class_name: 'User'
|
||||
belongs_to :last_modified_by, class_name: 'User'
|
||||
|
||||
validates :user, uniqueness: { scope: :user_group }
|
||||
end
|
||||
|
|
50
app/serializers/lists/user_group_serializer.rb
Normal file
50
app/serializers/lists/user_group_serializer.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Lists
|
||||
class UserGroupSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
|
||||
attributes :id, :name, :members, :created_by, :created_at, :updated_at, :urls, :team_users_count, :permissions
|
||||
|
||||
def members
|
||||
object.users.map do |u|
|
||||
{
|
||||
id: u.id,
|
||||
avatar: avatar_path(u, :icon_small),
|
||||
full_name: u.name
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def team_users_count
|
||||
object.team.users.size
|
||||
end
|
||||
|
||||
def created_by
|
||||
object.created_by.name
|
||||
end
|
||||
|
||||
def created_at
|
||||
I18n.l(object.created_at, format: :full_date)
|
||||
end
|
||||
|
||||
def updated_at
|
||||
I18n.l(object.updated_at, format: :full_date)
|
||||
end
|
||||
|
||||
def permissions
|
||||
{
|
||||
manage: can_manage_team?(object.team)
|
||||
}
|
||||
end
|
||||
|
||||
def urls
|
||||
{
|
||||
show: users_settings_team_user_group_path(object.team, object),
|
||||
unassigned_users: unassigned_users_users_settings_team_user_groups_path(object.team, user_group_id: object),
|
||||
assign_users: users_settings_team_user_group_user_group_memberships_path(object.team, object)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
50
app/services/lists/user_groups_service.rb
Normal file
50
app/services/lists/user_groups_service.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Lists
|
||||
class UserGroupsService < BaseService
|
||||
private
|
||||
|
||||
def fetch_records
|
||||
@records = @raw_data.joins(
|
||||
'LEFT OUTER JOIN users AS creators ' \
|
||||
'ON user_groups.created_by_id = creators.id'
|
||||
).left_joins(:user_group_memberships).includes(:users)
|
||||
.select('user_groups.* as user_groups')
|
||||
.select('creators.full_name AS created_by_user')
|
||||
.select('COUNT(user_groups.id) AS members_count')
|
||||
.group('user_groups.id, creators.full_name')
|
||||
end
|
||||
|
||||
def filter_records
|
||||
return unless @params[:search]
|
||||
|
||||
@records = @records.where_attributes_like(
|
||||
['user_groups.name'],
|
||||
@params[:search]
|
||||
)
|
||||
end
|
||||
|
||||
def sort_records
|
||||
return unless @params[:order]
|
||||
|
||||
sorted_column = sortable_columns[order_params[:column].to_sym]
|
||||
|
||||
if sorted_column == 'user_groups.members'
|
||||
sort_by = "members_count #{sort_direction(order_params)}"
|
||||
@records = @records.order(Arel.sql(sort_by))
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def sortable_columns
|
||||
@sortable_columns ||= {
|
||||
name: 'user_groups.name',
|
||||
members: 'user_groups.members',
|
||||
updated_at: 'user_groups.updated_at',
|
||||
created_by: 'created_by_user',
|
||||
created_at: 'user_groups.created_at'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
42
app/services/toolbars/user_groups_service.rb
Normal file
42
app/services/toolbars/user_groups_service.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Toolbars
|
||||
class UserGroupsService
|
||||
attr_reader :current_user
|
||||
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def initialize(current_user, team, user_group_ids: [])
|
||||
@current_user = current_user
|
||||
@team = team
|
||||
@user_groups = team.user_groups.where(id: user_group_ids)
|
||||
|
||||
@single = @user_groups.length == 1
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] if @user_groups.none?
|
||||
|
||||
[
|
||||
delete_action
|
||||
].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_team?(@team)
|
||||
|
||||
{
|
||||
name: 'delete',
|
||||
label: I18n.t('user_groups.index.toolbar.delete'),
|
||||
icon: 'sn-icon sn-icon-delete',
|
||||
path: users_settings_team_user_group_path(@team, @user_groups.first),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,4 +7,13 @@
|
|||
|
||||
<div class="content-pane flexible with-grey-background">
|
||||
<%= render partial: 'users/settings/teams/header' %>
|
||||
<div id="userGroupsTable" class="fixed-content-body user-groups-table-container">
|
||||
<user-groups-table
|
||||
actions-url="<%= actions_toolbar_users_settings_team_user_groups_path(@team) %>"
|
||||
data-source="<%= users_settings_team_user_groups_path(@team, format: :json) %>"
|
||||
users-url="<%= unassigned_users_users_settings_team_user_groups_path(@team) %>"
|
||||
create-url="<%= users_settings_team_user_groups_path(@team) if can_manage_team?(@team) %>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<%= javascript_include_tag 'vue_user_groups_table' %>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
json.data do
|
||||
json.array! @unassigned_users do |user|
|
||||
json.array! [
|
||||
user.id,
|
||||
user.name,
|
||||
{
|
||||
avatar: avatar_path(user, :icon_small)
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
|
@ -4189,7 +4189,34 @@ en:
|
|||
edit:
|
||||
head_title: "Edit protocol"
|
||||
no_keywords: "—"
|
||||
|
||||
user_groups:
|
||||
create:
|
||||
success: "Group created successfully."
|
||||
error: "There was a problem creating group. Please try again."
|
||||
delete:
|
||||
success: "Group deleted successfully."
|
||||
error: "There was a problem deleting group. Please try again."
|
||||
index:
|
||||
new_group: "New group"
|
||||
group_name: "Group name"
|
||||
members: "Members"
|
||||
created_by: "Created by"
|
||||
created_on: "Created on"
|
||||
updated_on: "Updated on"
|
||||
create_modal:
|
||||
title: "Create a new group"
|
||||
description: "You can add as many members from this workspace as you wish."
|
||||
name: "Group name"
|
||||
name_placeholder: "Add a name for your group"
|
||||
select_members: "Select members"
|
||||
select_members_placeholder: "Select for members"
|
||||
create_button: "Create group"
|
||||
delete_modal:
|
||||
title: "Delete group"
|
||||
description_html: "You are about to delete <b>%{group}</b>. All members added to this group will lose their access to all related content in this workspace.<br><br>Access added to members outside of this group will not be affected.<br><br>This action cannot be undone. <b>Are you sure you want to delete this group?</b>"
|
||||
confirm: "Delete group"
|
||||
toolbar:
|
||||
delete: "Delete"
|
||||
invite_users:
|
||||
to_team:
|
||||
title: "Invite members to %{team}"
|
||||
|
|
|
@ -149,8 +149,12 @@ Rails.application.routes.draw do
|
|||
resource :user_settings, only: %i(show update)
|
||||
|
||||
resources :teams, only: [] do
|
||||
resources :user_groups, only: %i(index create update destroy) do
|
||||
resources :user_groups, only: %i(index create update destroy show) do
|
||||
resources :user_group_memberships, only: %i(index create update destroy)
|
||||
collection do
|
||||
get :unassigned_users
|
||||
post :actions_toolbar
|
||||
end
|
||||
end
|
||||
|
||||
member do
|
||||
|
|
|
@ -74,7 +74,8 @@ const entryList = {
|
|||
vue_design_system_inputs: './app/javascript/packs/vue/design_system/inputs.js',
|
||||
vue_design_system_table: './app/javascript/packs/vue/design_system/table.js',
|
||||
vue_favorites_widget: './app/javascript/packs/vue/favorites_widget.js',
|
||||
vue_experiment_description_modal: './app/javascript/packs/vue/experiment_description_modal.js'
|
||||
vue_experiment_description_modal: './app/javascript/packs/vue/experiment_description_modal.js',
|
||||
vue_user_groups_table: './app/javascript/packs/vue/user_groups_table.js'
|
||||
};
|
||||
|
||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||
|
|
Loading…
Add table
Reference in a new issue