add new workspaces for users

This commit is contained in:
Adrian Zürcher
2026-02-19 10:49:12 +01:00
parent b726eb42dc
commit 6392877dc1
21 changed files with 744 additions and 80 deletions

View File

@@ -43,6 +43,14 @@ export function useUserTable() {
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'workspaces',
align: 'left' as const,
label: i18n.global.t('workspaces'),
field: 'workspaces',
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'expiration',
align: 'left' as const,
@@ -59,9 +67,9 @@ export function useUserTable() {
const loading = ref(false);
//updates user list from database
function updateUsers() {
async function updateUsers() {
loading.value = true;
appApi
await appApi
.get('users')
.then((resp) => {
if (resp.data === null) {

View File

@@ -9,7 +9,7 @@
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="users"
:rows="localUsers"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
@@ -23,17 +23,11 @@
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-btn v-if="writePermission" dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewUser') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
v-if="writePermission"
dense
flat
style="color: grey"
@@ -43,7 +37,9 @@
<q-tooltip>{{ $t('selectUserOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<div
v-if="selectOption && selected.length > 0 && user.isPermittedTo('userSettings', 'delete')"
>
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
@@ -71,13 +67,9 @@
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -101,7 +93,11 @@
<template v-slot:body-cell-role="props">
<q-td :props="props">
<q-select
:readonly="!user.isPermittedTo('userSettings', 'write') || !autorized(props.row)"
:readonly="
user.user?.id === props.row.id ||
!user.isPermittedTo('userSettings', 'write', props.row.role) ||
!autorized(props.row)
"
dense
v-model="props.row.role"
:options="localRoles"
@@ -110,16 +106,25 @@
></q-select>
</q-td>
</template>
<template v-slot:body-cell-workspaces="props">
<q-td :props="props">
<q-select
:readonly="props.row.id === user.user?.id || !autorized(props.row) || !writePermission"
dense
v-model="props.row.workspaces"
:options="localWorkspaces"
option-label="name"
multiple
@update:model-value="updateUser(props.row)"
></q-select>
</q-td>
</template>
<template v-slot:body-cell-expiration="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -148,7 +153,7 @@
query-id
v-on:update="(val) => updateUser(val)"
></EditOneDialog>
<EditAllDialog ref="editAllDialog" :roles="roles" v-on:update="updateUsers"></EditAllDialog>
<EditAllDialog ref="editAllDialog" :roles="localRoles" v-on:update="updateUsers"></EditAllDialog>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
@@ -177,34 +182,73 @@ import { i18n } from 'src/boot/lang';
import { useUserStore } from 'src/vueLib/login/userStore';
import ChangePassword from 'src/vueLib/login/ChangePassword.vue';
import SearchableInput from '../components/SearchableInput.vue';
import { useWorkspaceTable } from '../workspaces/WorkspaceTable';
const { NotifyResponse } = useNotify();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const { updateWorkspaces } = useWorkspaceTable();
const user = useUserStore();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const localRoles = computed(() => {
return roles.value.filter((role) => {
if (user.user?.role.role.includes('admin') || !user.user?.role.role.includes('admin'))
return role;
const localUsers = computed(() => {
return users.value.filter((u) => {
if (user.isAdmin() || user.user?.id === u.id) return user;
const roleP = u.role?.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
const notInWorkspace = u.workspaces?.some((w) => currentUser.value.workspaces.includes(w.name));
if (
roleP === undefined ||
userP === undefined ||
(!notInWorkspace && u.workspaces?.length !== 0)
)
return;
if (userP > roleP) return user;
});
});
const localRoles = computed(() => {
return roles.value.filter((role) => {
const roleP = role.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
if (roleP === undefined || userP === undefined) return;
if (userP < roleP) return;
if (user.isAdmin() || !role.role.includes('admin')) return role;
});
});
const localWorkspaces = computed(() => {
return users.value.filter((u) => u.id === user.user?.id)[0]?.workspaces;
});
const selectOption = ref(false);
const selected = ref<Users>([]);
const openSubmenu = ref(false);
const filter = ref('');
const currentUser = ref();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const user = useUserStore();
const changePwdDialog = ref();
const writePermission = computed(() => user.isPermittedTo('userSettings', 'write'));
//load on mounting page
onMounted(() => {
onMounted(async () => {
loading.value = true;
currentUser.value = user.user;
updateUsers();
updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateUsers().finally(() => {
const targetUser = users.value.find((u) => u.id === currentUser.value.id);
if (targetUser && targetUser.workspaces) {
currentUser.value.workspaces = targetUser.workspaces.map((ws) => ws.name);
}
});
await updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
// get workspaces of current user
});
//check authorization
@@ -264,7 +308,7 @@ function removeUser(...removeUsers: Users) {
appApi
.post('users/delete?id=' + currentUser.value.id, { ids: userIds })
.then(() => {
updateUsers();
updateUsers().catch((err) => NotifyResponse(err, 'error'));
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
@@ -278,6 +322,7 @@ function updateUser(user: User) {
if (user.role?.id) {
user.roleId = user.role?.id;
}
appApi
.post('/users/update', user)
.then(() => NotifyResponse(i18n.global.t('userUpdated')))

View File

@@ -0,0 +1,78 @@
import { appApi } from 'src/boot/axios';
import { ref, computed } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
import type { Workspaces } from 'src/vueLib/models/workspaces';
// import { useUserStore } from 'src/vueLib/login/userStore';
// import { useLogin } from 'src/vueLib/login/useLogin';
export const workspaces = ref<Workspaces>([]);
export function useWorkspaceTable() {
const pagination = ref({
sortBy: 'name',
descending: false,
page: 1,
rowsPerPage: 20,
});
const columns = computed(() => [
{
name: 'name',
align: 'left' as const,
label: i18n.global.t('name'),
field: 'name',
sortable: true,
},
{
name: 'description',
align: 'left' as const,
label: i18n.global.t('description'),
field: 'description',
style: 'width: 120px; max-width: 120px;',
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]);
const { NotifyResponse } = useNotify();
const loading = ref(false);
// const userStore = useUserStore();
// const login = useLogin();
//updates user list from database
async function updateWorkspaces() {
loading.value = true;
await appApi
.get('workspaces?id=0')
.then((resp) => {
if (resp.data === null) {
workspaces.value = [];
return;
}
workspaces.value = resp.data as Workspaces;
if (workspaces.value === null) {
workspaces.value = [];
return;
}
})
.catch((err) => {
NotifyResponse(err, 'error');
})
.finally(() => {
loading.value = false;
});
await appApi
.post('users/update', { id: 1, workspaces: workspaces.value })
.catch((err) => NotifyResponse(err, 'error'));
}
return {
workspaces,
pagination,
columns,
loading,
updateWorkspaces,
};
}

View File

@@ -0,0 +1,226 @@
<template>
<q-table
flat
bordered
ref="tableRef"
title="Workspaces"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="workspaces"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
dense
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-tooltip>{{ $t('addNewWorkspace') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectWorkspaceOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<!-- top right of table-->
<template v-slot:top-right>
<SearchableInput v-model="filter" :placeholder="$t('search')" />
</template>
<!-- table body content-->
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="user.isPermittedTo('userSettings', 'write') ? 'cursor: pointer' : ''"
@click="
user.isPermittedTo('userSettings', 'write')
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
>
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('userSettings', 'delete')"
flat
dense
icon="delete"
color="negative"
@click="openRemoveDialog(props.row)"
>
<q-tooltip> {{ $t('delete') }} </q-tooltip>
</q-btn>
</q-td>
</template>
</q-table>
<EditOneDialog
ref="editOneDialog"
endpoint="workspaces/update"
query-id
v-on:update="updateWorkspaces"
></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update="updateWorkspaces"></EditAllDialog>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removeWorkspace(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import type { Workspaces, Workspace } from 'src/vueLib/models/workspaces';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/WorkspaceEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useWorkspaceTable } from './WorkspaceTable';
import { i18n } from 'src/boot/lang';
import { QTable } from 'quasar';
import { useUserStore } from 'src/vueLib/login/userStore';
import SearchableInput from '../components/SearchableInput.vue';
import type { User } from 'src/vueLib/models/user';
import { workspace } from 'src/vueLib/models/settings';
const { NotifyResponse } = useNotify();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
const selected = ref<Workspaces>([]);
const openSubmenu = ref(false);
const currentUser = ref<User>();
const filter = ref('');
const user = useUserStore();
const { workspaces, pagination, loading, columns, updateWorkspaces } = useWorkspaceTable();
//load on mounting page
onMounted(() => {
loading.value = true;
if (user.user) {
currentUser.value = user.user;
}
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
});
// opens dialog for all workspace values
function openSingleValueDialog(label: string, field: string, workspace: Workspace) {
editOneDialog.value?.open(label, field, workspace);
}
//opens dialog for one value
function openAllValueDialog(workspace: Workspace | null, typ?: 'permissions') {
editAllDialog.value?.open(workspace, typ);
}
//opens remove dialog
function openRemoveDialog(...workspaces: Workspaces) {
if (workspaces.length === 1) {
deleteText.value = "'" + workspaces[0]?.name + "'";
} else {
deleteText.value = String(workspaces.length) + ' ' + i18n.global.t('workspaces');
}
okDialog.value?.open(workspaces);
}
//remove workspace from database
function removeWorkspace(...removeWorkspaces: Workspaces) {
const workspaces: Workspace[] = [];
removeWorkspaces.forEach((workspace: Workspace) => {
if (workspace.id === currentUser.value?.settings?.workspace) {
NotifyResponse(i18n.global.t('notPossibleToDeleteUsedWorkspace'), 'error');
} else if (workspace.id) {
workspaces.push(workspace);
}
});
appApi
.post('workspaces/delete', {
workspaces: workspaces,
})
.then(() => {
const storageWorkspace = localStorage.getItem('workspace');
if (workspaces.some((w) => w.uuid === storageWorkspace)) {
workspace.value = '';
localStorage.removeItem('workspace');
}
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
if (workspaces.length === 1) {
NotifyResponse("'" + workspaces[0]?.name + "' " + i18n.global.t('deleted'), 'warning');
} else {
NotifyResponse(i18n.global.t('deleteWorkspaces'), 'warning');
}
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
selectOption.value = false;
});
}
</script>
<style>
.bigger-table-text .q-table__middle td {
font-size: 14px;
}
.bigger-table-text .q-table__top,
.bigger-table-text .q-table__bottom,
.bigger-table-text th {
font-size: 14px;
}
</style>