196 lines
5.7 KiB
Vue
196 lines
5.7 KiB
Vue
<script setup>
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
const i18n = useI18n();
|
|
const t = i18n.t;
|
|
|
|
import { ref, onMounted, useTemplateRef, inject, computed } from 'vue';
|
|
import { Button, Menu, TableView, InputDialog, TextInput } from '@cloudron/pankow';
|
|
import Section from '../components/Section.vue';
|
|
import GroupDialog from '../components/GroupDialog.vue';
|
|
import UsersModel from '../models/UsersModel.js';
|
|
import GroupsModel from '../models/GroupsModel.js';
|
|
import ProfileModel from '../models/ProfileModel.js';
|
|
|
|
const usersModel = UsersModel.create();
|
|
const groupsModel = GroupsModel.create();
|
|
const profileModel = ProfileModel.create();
|
|
|
|
const actionMenuModel = ref([]);
|
|
const actionMenuElement = useTemplateRef('actionMenuElement');
|
|
|
|
const groupsColumns = {
|
|
name: {
|
|
label: t('users.groups.name'),
|
|
nowrap: true,
|
|
sort: true,
|
|
},
|
|
users: {
|
|
label: t('users.groups.users'),
|
|
sort: true,
|
|
nowrap: true,
|
|
hideMobile: true,
|
|
width: '500px',
|
|
},
|
|
actions: {
|
|
width: '55px',
|
|
}
|
|
};
|
|
|
|
function onGroupActionMenu(group, event) {
|
|
actionMenuModel.value = [{
|
|
icon: 'fa-solid fa-pencil-alt',
|
|
label: t('main.action.edit'),
|
|
action: onEditOrAddGroup.bind(null, group),
|
|
}, {
|
|
separator: true,
|
|
}, {
|
|
icon: 'fa-solid fa-trash-alt',
|
|
label: t('main.action.remove'),
|
|
action: onRemoveGroup.bind(null, group),
|
|
}];
|
|
|
|
actionMenuElement.value.open(event, event.currentTarget);
|
|
}
|
|
|
|
const profile = ref({});
|
|
const busy = ref(true);
|
|
const users = ref([]);
|
|
const usersById = ref({});
|
|
const groups = ref([]);
|
|
const groupsById = ref({});
|
|
const roles = ref([]);
|
|
|
|
const features = inject('features');
|
|
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
|
|
|
|
const inputDialog = useTemplateRef('inputDialog');
|
|
const groupDialog = useTemplateRef('groupDialog');
|
|
|
|
async function refreshUsers() {
|
|
const [error, result] = await usersModel.list();
|
|
if (error) return console.error(error);
|
|
|
|
usersById.value = {};
|
|
users.value = result;
|
|
result.forEach(user => {
|
|
usersById.value[user.id] = user;
|
|
});
|
|
}
|
|
|
|
async function refreshGroups() {
|
|
const [error, result] = await groupsModel.list();
|
|
if (error) return console.error(error);
|
|
|
|
groupsById.value = {};
|
|
groups.value = result;
|
|
result.forEach(group => {
|
|
groupsById.value[group.id] = group;
|
|
});
|
|
}
|
|
|
|
function groupMembers(group) {
|
|
return group.userIds.filter(function (uid) { return !!usersById.value[uid]; }).map(function (uid) { return usersById.value[uid].username || usersById.value[uid].email; }).join(' ');
|
|
}
|
|
|
|
const searchFilter = ref('');
|
|
|
|
const filteredGroups = computed(() => {
|
|
if (!searchFilter.value) return groups.value;
|
|
|
|
return groups.value.filter(g => {
|
|
const search = searchFilter.value.toLowerCase();
|
|
|
|
if (g.name.toLowerCase().indexOf(search) !== -1) return true;
|
|
if (groupMembers(g).toLowerCase().indexOf(search) !== -1) return true;
|
|
|
|
return false;
|
|
});
|
|
});
|
|
|
|
function onEditOrAddGroup(group = null) {
|
|
if (group || features.value.userGroups) groupDialog.value.open(group);
|
|
else subscriptionRequiredDialog.value.open();
|
|
}
|
|
|
|
async function onRemoveGroup(group) {
|
|
const yes = await inputDialog.value.confirm({
|
|
title: t('users.deleteGroupDialog.title'),
|
|
message: t('users.deleteGroupDialog.description', { name: group.name, memberCount: group.userIds.length }),
|
|
confirmStyle: 'danger',
|
|
confirmLabel: t('users.deleteGroupDialog.deleteAction'),
|
|
rejectLabel: t('main.dialog.cancel'),
|
|
rejectStyle: 'secondary'
|
|
});
|
|
|
|
if (!yes) return;
|
|
|
|
const [error] = await groupsModel.remove(group.id);
|
|
if (error) console.error(error);
|
|
|
|
await refreshGroups();
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const [error, result] = await profileModel.get();
|
|
if (error) return console.error(error);
|
|
|
|
profile.value = result;
|
|
|
|
await refreshUsers();
|
|
await refreshGroups();
|
|
|
|
// Order matters for permissions used in canEdit
|
|
roles.value = [
|
|
{ id: 'user', name: t('users.role.user'), disabled: false },
|
|
{ id: 'usermanager', name: t('users.role.usermanager'), disabled: false },
|
|
{ id: 'mailmanager', name: t('users.role.mailmanager'), disabled: false },
|
|
{ id: 'admin', name: t('users.role.admin'), disabled: !profile.value.isAtLeastAdmin },
|
|
{ id: 'owner', name: t('users.role.owner'), disabled: !profile.value.isAtLeastOwner }
|
|
];
|
|
|
|
busy.value = false;
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="content">
|
|
<Menu ref="actionMenuElement" :model="actionMenuModel" />
|
|
<InputDialog ref="inputDialog" />
|
|
<GroupDialog ref="groupDialog" @success="refreshGroups()"/>
|
|
|
|
<Section :title="$t('main.navbar.groups')" :title-badge="!features.userGroups ? 'Upgrade' : ''">
|
|
<template #header-title-extra>
|
|
<span style="font-weight: normal; font-size: 14px">({{ busy ? '-' : filteredGroups.length }})</span>
|
|
</template>
|
|
|
|
|
|
<template #header-buttons>
|
|
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="searchFilter"/>
|
|
<Button @click="onEditOrAddGroup()">{{ $t('main.action.add') }}</Button>
|
|
</template>
|
|
|
|
<TableView :columns="groupsColumns" :model="filteredGroups" :busy="busy" :fixed-layout="true" :placeholder="$t(searchFilter ? 'users.groups.noMatchesPlaceholder' : 'users.groups.emptyPlaceholder')">
|
|
<template #name="group">
|
|
{{ group.name }} <i v-if="group.source" class="far fa-address-book" v-tooltip="$t('users.groups.externalLdapTooltip')"></i>
|
|
</template>
|
|
<template #users="group">{{ groupMembers(group) }}</template>
|
|
<template #actions="group">
|
|
<div style="text-align: right;">
|
|
<Button tool plain secondary @click.capture="onGroupActionMenu(group, $event)" icon="fa-solid fa-ellipsis" />
|
|
</div>
|
|
</template>
|
|
</TableView>
|
|
</Section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
.group-label {
|
|
padding: 0 5px;
|
|
}
|
|
|
|
</style>
|