release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
16 changed files with 923 additions and 539 deletions
Showing only changes of commit 741216ac3e - Show all commits

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright 2021 Tim Gröger | Flaschengeist Developers
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,47 +1,62 @@
# Flaschengeist (flaschengeist-frontend)
# Flaschengeist (frontend)
Dynamischen Managementsystem für Studentenclubs
Modular student club administration system, licensed under the MIT license.
## Installation
### Install the dependencies
## Install the dependencies
```bash
yarn install
```
## Plugins
### Build a Plugin
### Configure Plugins
You can activate and deactive Plugins in `src/boot/plugins.ts`.
You have to set the name of the Plugin into `config.loadModules`.
### Build the application
```bash
yarn quasar build
```
## Development
### Icons used
We are using the `mdi-v5` icon set, so feel free to use any icon from it.
A list can be found [here](https://materialdesignicons.com/)
### Commands useful for development
#### Start the app in development mode
Provides hot-code reloading, error reporting, etc.
```bash
yarn quasar dev
```
#### File linting
```bash
yarn run lint
```
### Plugins
#### Build a Plugin
A Flaschengeist-Frontend-Plugin should be placed in `src/plugins`.
It needs a `plugin.ts` File which exports a plugin with the following interface:
```
name: string;
mainRoutes?: PluginRouteConfig[];
outRoutes?: PluginRouteConfig[];
store?: Map<string, Module<any, StateInterface>>;
requiredModules: string[];
version: string;
```
You have to import `FG_Plugin` from `plugins.d.ts`.
### Configure Plugin
You can activate and deactive Plugins in `src/boot/plugins.ts`. You have to set the name of the Plugin into `config.loadModules`.
The order of the plugins is importend!
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
yarn quasar dev
```
### Lint the files
```bash
yarn run lint
```
### Build the app for production
```bash
yarn quasar build
```
<!--
### Customize the configuration
See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js).
-->

View File

@ -1,10 +1,15 @@
{
"name": "flaschengeist-frontend",
"version": "0.1.0-alpha.1",
"description": "Dynamischen Managementsystem für Studentenclubs",
"productName": "Flaschengeist",
"author": "Tim Gröger <tim@groeger-clan.de>",
"private": true,
"license": "MIT",
"version": "2.0.0-alpha.1",
"productName": "Flaschengeist",
"name": "flaschengeist-frontend",
"author": "Tim Gröger <flaschengeist@wu5.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist",
"description": "Modular student club administration system",
"bugs": {
"url" : "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
},
"scripts": {
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
"lint": "eslint --ext .js,.ts,.vue ./src"
@ -12,32 +17,31 @@
"dependencies": {
"axios": "^0.21.1",
"cordova": "^10.0.0",
"core-js": "^3.9.1",
"pinia": "^2.0.0-alpha.7",
"pinia": "^2.0.0-alpha.10",
"quasar": "^2.0.0-beta.11"
},
"devDependencies": {
"@quasar/app": "^3.0.0-beta.10",
"@quasar/extras": "^1.10.2",
"@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
"@types/node": "^12.20.7",
"@types/webpack": "^4.41.27",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.20.0",
"@typescript-eslint/parser": "^4.20.0",
"eslint": "^7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^7.8.0",
"eslint-webpack-plugin": "^2.5.3",
"prettier": "^2.2.1",
"typescript": "^4.2.3"
},
"prettier": {
"singleQuote": true,
"semi": true,
"printWidth": 100,
"arrowParens": "always"
},
"devDependencies": {
"@quasar/app": "^3.0.0-beta.10",
"@quasar/extras": "^1.10.0",
"@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
"@types/node": "^12.20.6",
"@types/webpack": "^4.41.26",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^7.7.0",
"eslint-webpack-plugin": "^2.5.2",
"prettier": "^2.2.1",
"typescript": "^4.2.3"
},
"browserslist": [
"last 10 Chrome versions",
"last 10 Firefox versions",

View File

@ -37,16 +37,16 @@ module.exports = configure(function (/* ctx */) {
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
'mdi-v5',
// 'fontawesome-v5',
// 'eva-icons',
// 'themify',
// 'fontawesome-v5',
// 'ionicons-v4',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
// 'material-icons',
'mdi-v5',
// 'themify',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
],
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
@ -74,7 +74,7 @@ module.exports = configure(function (/* ctx */) {
chainWebpack (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{
extensions: [ 'js', 'vue' ],
extensions: [ 'ts', 'js', 'vue' ],
exclude: 'node_modules'
}])
},
@ -90,7 +90,7 @@ module.exports = configure(function (/* ctx */) {
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
framework: {
iconSet: 'material-icons', // Quasar icon set
iconSet: 'mdi-v5', // Quasar icon set
lang: 'de', // Quasar language pack
config: {
dark: 'auto',
@ -134,7 +134,7 @@ module.exports = configure(function (/* ctx */) {
manifest: {
name: 'Flaschengeist',
short_name: 'Flaschengeist',
description: 'Dynamischen Managementsystem für Studentenclubs',
description: 'Modular student club administration system',
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',

View File

@ -7,10 +7,11 @@
:placeholder="placeholder"
:rules="customRules"
:clearable="clearable"
v-bind="attrs"
@clear="dateTime = ''"
>
<template #append>
<q-icon v-if="'date' || type == 'datetime'" name="event" class="cursor-pointer">
<q-icon v-if="'date' || type == 'datetime'" name="mdi-calendar" class="cursor-pointer">
<q-popup-proxy ref="qDateProxy" transition-show="scale" transition-hide="scale">
<q-date v-model="date" mask="YYYY-MM-DD">
<div class="row items-center justify-end">
@ -58,7 +59,7 @@ export default defineComponent({
},
},
emits: { 'update:modelValue': (date?: Date) => !!date || !date },
setup(props, { emit }) {
setup(props, { emit, attrs }) {
const customRules = computed(() => [
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
...props.rules,
@ -138,12 +139,13 @@ export default defineComponent({
}
return {
attrs,
clearable,
date,
time,
dateTime,
customRules,
date,
dateTime,
placeholder,
time,
};
},
});

View File

@ -0,0 +1,47 @@
<template>
<q-input v-model="password" v-bind="attrs" :label="label" :type="type">
<template #append><q-icon :name="name" class="cursor-pointer" @click="toggle" /></template
></q-input>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
name: 'PasswordInput',
props: {
modelValue: {
type: String,
required: true,
},
label: {
type: String,
default: '',
},
},
emits: {
'update:modelValue': (value: string) => !!value,
},
setup(props, { emit, attrs }) {
const isPassword = ref(true);
const type = computed(() => (isPassword.value ? 'password' : 'text'));
const name = computed(() => (isPassword.value ? 'mdi-eye-off' : 'mdi-eye'));
const password = computed({
get: () => props.modelValue,
set: (value: string) => emit('update:modelValue', value),
});
function toggle() {
isPassword.value = !isPassword.value;
}
return {
attrs,
isPassword,
name,
password,
toggle,
type,
};
},
});
</script>

View File

@ -1,15 +1,8 @@
<template>
<q-layout view="hHh lpr lFf">
<q-layout view="hHh Lpr lff">
<q-header elevated class="bg-primary text-white">
<q-toolbar>
<q-btn
v-if="!leftDrawerOpen"
dense
flat
round
icon="menu"
@click="leftDrawerOpen = !leftDrawerOpen"
/>
<q-btn dense flat round icon="mdi-menu" @click="openMenu" />
<q-toolbar-title>
<router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit">
@ -21,7 +14,7 @@
</q-toolbar-title>
<!-- Hier kommen die Shortlinks hin -->
<q-btn icon="message" flat dense
<q-btn icon="mdi-message-bulleted" flat dense
><q-badge color="negative" floating>{{ notifications.length }}</q-badge>
<q-menu style="max-height: 400px; overflow: auto">
<q-btn
@ -50,12 +43,11 @@
</q-header>
<q-drawer
v-model="leftDrawerOpen"
show-if-above
v-model="leftDrawer"
side="left"
bordered
:mini="leftDrawerMini"
@click.capture="leftDrawerClicker"
@click.capture="openMenu"
>
<!-- Plugins -->
<q-list>
@ -66,28 +58,13 @@
/>
<q-separator />
<!-- Plugin functions -->
<essential-link
v-for="(entry, index) in subLinks"
:key="'childPlugin' + index"
:entry="entry"
/>
</q-list>
<div class="q-mini-drawer-hide absolute" style="top: 15px; right: -11px">
<q-btn
size="sm"
dense
round
unelevated
color="secondary"
icon="chevron_left"
@click="leftDrawerMini = true"
/>
</div>
<q-separator />
<essential-link
v-for="(entry, index) in essentials"
:key="'essential' + index"
@ -126,7 +103,7 @@ export default defineComponent({
const router = useRouter();
const mainStore = useMainStore();
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
const leftDrawer = ref(false);
const leftDrawer = ref(true);
const leftDrawerMini = ref(false);
const shortcuts = flaschengeist?.shortcuts || [];
const mainLinks = flaschengeist?.menuLinks || [];
@ -135,22 +112,27 @@ export default defineComponent({
const useNative = 'Notification' in window && window.Notification !== undefined;
const noPermission = ref(!useNative || window.Notification.permission !== 'granted');
onBeforeMount(() => pollNotification());
onBeforeUnmount(() => window.clearInterval(polling.value));
const leftDrawerOpen = computed({
get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
set: (val: boolean) => (leftDrawer.value = val),
});
const subLinks = computed(() => {
const matched = router.currentRoute.value.matched[1];
return flaschengeist?.menuLinks.find((link) => matched.name == link.link)?.children;
});
function leftDrawerClicker() {
if (leftDrawerMini.value) {
leftDrawerMini.value = false;
onBeforeMount(() => {
polling.value = window.setInterval(() => pollNotification(), config.pollingInterval);
pollNotification();
});
onBeforeUnmount(() => window.clearInterval(polling.value));
function openMenu(event: { target: HTMLInputElement }) {
if (event.target.nodeName === 'DIV') leftDrawerMini.value = false;
else {
if (!leftDrawer.value || leftDrawerMini.value) {
leftDrawer.value = true;
leftDrawerMini.value = false;
} else {
leftDrawerMini.value = Screen.gt.sm && !leftDrawerMini.value;
leftDrawer.value = leftDrawerMini.value;
}
}
}
@ -170,30 +152,28 @@ export default defineComponent({
}
function pollNotification() {
polling.value = window.setInterval(() => {
void mainStore
.loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist)
.then((notifications) => {
if (useNative && !noPermission.value)
notifications.forEach(
(notif) =>
new window.Notification(notif.text, {
timestamp: notif.time.getTime(),
})
);
});
}, config.pollingInterval);
void mainStore
.loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist)
.then((notifications) => {
if (useNative && !noPermission.value)
notifications.forEach(
(notif) =>
new window.Notification(notif.text, {
timestamp: notif.time.getTime(),
})
);
});
}
return {
essentials,
leftDrawerOpen,
leftDrawer,
leftDrawerMini,
leftDrawerClicker,
logout,
mainLinks,
notifications,
noPermission,
openMenu,
remove,
requestPermission,
shortcuts,

View File

@ -6,20 +6,21 @@
</q-toolbar>
<q-card-section>
<q-form ref="LoginForm" class="q-gutter-md" @submit="doLogin">
<q-form class="q-gutter-md" @submit="doLogin">
<q-input
v-model="userid"
filled
label="Benutzername oder E-Mail"
:rules="rules"
autocomplete="username"
:rules="[notEmpty]"
tabindex="1"
/>
<q-input
<password-input
v-model="password"
filled
type="password"
label="Password"
:rules="rules"
label="Passwort"
autocomplete="cureent-password"
:rules="[notEmpty]"
tabindex="2"
/>
<div class="row justify-between">
@ -54,15 +55,18 @@
</template>
<script lang="ts">
import { useMainStore } from 'src/stores';
import { useRouter } from 'vue-router';
import { Loading, Notify } from 'quasar';
import { useMainStore } from 'src/stores';
import { defineComponent, ref } from 'vue';
import { setBaseURL, api } from 'boot/axios';
import { notEmpty } from 'src/utils/validators';
import { useUserStore } from 'src/plugins/user/store';
import PasswordInput from 'src/components/utils/PasswordInput.vue';
export default defineComponent({
name: 'Login',
components: { PasswordInput },
setup() {
const mainStore = useMainStore();
const mainRoute = { name: 'dashboard' };
@ -71,7 +75,6 @@ export default defineComponent({
/* Stuff for the real login page */
const userid = ref('');
const password = ref('');
const rules = [(val: string) => (val && val.length > 0) || 'Feld darf nicht leer sein!'];
const server = ref<string | undefined>(api.defaults.baseURL);
const visible = ref(false);
@ -138,15 +141,15 @@ export default defineComponent({
}
return {
userid,
password,
changeUrl,
doLogin,
doReset,
rules,
server,
changeUrl,
visible,
notEmpty,
openServerSettings,
password,
server,
userid,
visible,
};
},
});

View File

@ -1,5 +1,5 @@
<template>
<div>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab
v-for="(tabindex, index) in tabs"
@ -37,32 +37,30 @@
</div>
</q-list>
</q-drawer>
<q-page padding class="fit row justify-left q-col-gutter-sm">
<q-tab-panels
v-model="tab"
style="background-color: transparent"
class="q-pa-none col-12"
animated
>
<q-tab-panel name="add" class="q-px-xs">
<BalanceAdd
@open-history="
showDrawer = !showDrawer;
show = true;
"
/>
</q-tab-panel>
<q-tab-panel name="transfer" class="q-px-xs">
<BalanceTransfer
@open-history="
showDrawer = !showDrawer;
show = true;
"
/>
</q-tab-panel>
</q-tab-panels>
</q-page>
</div>
<q-tab-panels
v-model="tab"
style="background-color: transparent"
class="q-pa-none col-12"
animated
>
<q-tab-panel name="add" class="q-px-xs">
<BalanceAdd
@open-history="
showDrawer = !showDrawer;
show = true;
"
/>
</q-tab-panel>
<q-tab-panel name="transfer" class="q-px-xs">
<BalanceTransfer
@open-history="
showDrawer = !showDrawer;
show = true;
"
/>
</q-tab-panel>
</q-tab-panels>
</q-page>
</template>
<script lang="ts">

View File

@ -1,10 +1,10 @@
<template>
<q-card style="text-align: center">
<q-card-section class="row justify-center content-stretch">
<div class="col-4">
<div v-if="avatar" class="col-4">
<div style="width: 100%; padding-bottom: 100%; position: relative">
<q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
<img :src="avatarLink" />
<img :src="avatarLink" :onerror="error" />
</q-avatar>
</div>
</div>
@ -39,9 +39,14 @@ export default defineComponent({
// Ensure users are loaded,so we can query birthdays
onMounted(() => userStore.getUsers(false));
const name = ref(mainStore.currentUser.display_name);
const avatar = ref(true);
const name = ref(mainStore.currentUser.firstname);
const avatarLink = ref(mainStore.currentUser.avatar_url);
function error() {
avatar.value = false;
}
function userHasBirthday(user: FG.User) {
const today = new Date();
return (
@ -61,7 +66,7 @@ export default defineComponent({
.filter((user) => user.userid !== mainStore.currentUser.userid)
);
return { avatarLink, name, hasBirthday, birthday };
return { avatar, avatarLink, error, name, hasBirthday, birthday };
},
});
</script>

View File

@ -2,44 +2,48 @@
<q-form @submit="save" @reset="reset">
<q-card-section class="fit row justify-start content-center items-center">
<q-input
v-model="user_model.firstname"
v-model="userModel.firstname"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Vorname"
:rules="[notEmpty]"
autocomplete="given-name"
filled
/>
<q-input
v-model="user_model.lastname"
v-model="userModel.lastname"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Nachname"
:rules="[notEmpty]"
autocomplete="family-name"
filled
/>
<q-input
v-model="user_model.display_name"
v-model="userModel.display_name"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Angezeigter Name"
:rules="[notEmpty]"
autocomplete="nickname"
filled
/>
<q-input
v-model="user_model.mail"
v-model="userModel.mail"
class="col-xs-12 col-sm-6 q-pa-sm"
label="E-Mail"
:rules="[isEmail, notEmpty]"
autocomplete="email"
filled
/>
<q-input
v-model="user_model.userid"
v-model="userModel.userid"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Benutzername"
:readonly="!newUser"
:rules="[isUseridUsed, notEmpty]"
:rules="newUser ? [isFreeUID, notEmpty] : []"
autocomplete="username"
filled
/>
<q-select
v-model="user_model.roles"
v-model="userModel.roles"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Rollen"
filled
@ -51,9 +55,10 @@
option-value="name"
/>
<IsoDateInput
v-model="user_model.birthday"
v-model="userModel.birthday"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Geburtstag"
autocomplete="bday"
/>
<q-file
v-model="avatar"
@ -72,31 +77,22 @@
</q-card-section>
<q-separator v-if="!newUser" />
<q-card-section v-if="!newUser" class="fit row justify-start content-center items-center">
<q-input
<PasswordInput
v-if="isCurrentUser"
v-model="password"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
label="Password"
type="password"
hint="Password muss immer eingetragen werden"
:rules="[notEmpty]"
filled
label="Passwort"
autocomplete="current-password"
class="col-xs-12 col-sm-6 q-pa-sm"
hint="Passwort muss immer eingetragen werden"
/>
<q-input
v-model="new_password"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
<PasswordInput
v-model="newPassword"
filled
label="Neues Password"
type="password"
filled
/>
<q-input
v-model="new_password2"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
label="Wiederhole neues Password"
type="password"
:disable="new_password.length == 0"
:rules="[samePassword]"
filled
autocomplete="new-password"
class="col-xs-12 col-sm-6 q-pa-sm"
/>
</q-card-section>
<q-card-actions align="right">
@ -109,14 +105,16 @@
<script lang="ts">
import { Notify } from 'quasar';
import { hasPermission } from 'src/utils/permission';
import { notEmpty, isEmail } from 'src/utils/validators';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { defineComponent, computed, ref, onBeforeMount, PropType } from 'vue';
import PasswordInput from 'src/components/utils/PasswordInput.vue';
import { defineComponent, computed, ref, onBeforeMount, PropType, watch } from 'vue';
import { useUserStore } from '../../store';
import { useMainStore } from 'src/stores';
export default defineComponent({
name: 'MainUserSettings',
components: { IsoDateInput },
components: { IsoDateInput, PasswordInput },
props: {
user: {
required: true,
@ -131,17 +129,25 @@ export default defineComponent({
const userStore = useUserStore();
const mainStore = useMainStore();
const user_model = ref(props.user);
onBeforeMount(() => {
void userStore.getRoles(false);
});
const isCurrentUser = computed(() => user_model.value.userid === mainStore.currentUser.userid);
const password = ref('');
const newPassword = ref('');
const avatar = ref<File | FileList | string[]>();
const userModel = ref(props.user);
const canSetRoles = computed(() => hasPermission('users_set_roles'));
const allRoles = computed(() => userStore.roles.map((role) => role.name));
const isCurrentUser = computed(() => userModel.value.userid === mainStore.currentUser.userid);
/* Reset model if props changed */
watch(
() => props.user,
() => (userModel.value = props.user)
);
const avatar = ref([] as string[]);
function onAvatarRejected() {
Notify.create({
group: false,
@ -151,84 +157,61 @@ export default defineComponent({
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
avatar.value = [];
avatar.value = undefined;
}
const allRoles = computed(() => userStore.roles.map((role) => role.name));
const password = ref('');
const new_password = ref('');
const new_password2 = ref('');
function save() {
let changed = user_model.value;
let changed = userModel.value;
if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday);
changed = Object.assign(changed, {
password: password.value,
});
if (new_password.value != '') {
if (newPassword.value != '') {
changed = Object.assign(changed, {
new_password: new_password.value,
new_password: newPassword.value,
});
}
emit('update:user', changed);
if (avatar.value && (avatar.value.length > 0 || avatar.value instanceof File))
userStore.uploadAvatar(changed, avatar.value[0]).catch((response: Response) => {
if (response && response.status == 400) {
onAvatarRejected();
}
});
if (avatar.value)
userStore
.uploadAvatar(changed, avatar.value instanceof File ? avatar.value : avatar.value[0])
.catch((response: Response) => {
if (response && response.status == 400) {
onAvatarRejected();
}
});
reset();
}
function reset() {
user_model.value = props.user;
userModel.value = props.user;
password.value = '';
new_password.value = '';
new_password2.value = '';
newPassword.value = '';
}
function samePassword(val: string) {
return val == new_password.value || 'Passwörter sind nicht identisch!';
}
function notEmpty(val: string) {
return !!val || 'Feld darf nicht leer sein!';
}
function isEmail(val: string | null) {
function isFreeUID(val: string) {
return (
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht valide.'
);
}
function isUseridUsed(val: string) {
return (
!userStore.users.find((user: FG.User) => {
return user.userid == val;
}) ||
!props.newUser ||
userStore.users.findIndex((user) => user.userid === val) === -1 ||
'Benutzername ist schon vergeben'
);
}
return {
avatar,
user_model,
onAvatarRejected,
allRoles,
avatar,
canSetRoles,
password,
new_password,
new_password2,
samePassword,
isCurrentUser,
isEmail,
isFreeUID,
newPassword,
notEmpty,
isUseridUsed,
save,
onAvatarRejected,
password,
reset,
save,
userModel,
};
},
});

View File

@ -22,15 +22,15 @@
/>
</q-card-section>
<q-separator />
<q-card-section v-if="role" class="fit row justify-start content-center items-center">
<q-scroll-area style="height: 20em; width: 100%">
<q-input v-if="role.id != -1" v-model="newRoleName" filled label="neuer Name" />
<q-card-section v-if="role">
<q-input v-if="role.id !== -1" v-model="newRoleName" filled label="neuer Name" />
<q-scroll-area style="height: 40vh; width: 100%" class="background-like-input">
<q-option-group
:model-value="role.permissions"
:options="permissions"
color="primary"
type="checkbox"
@input="updatePermissions"
@update:modelValue="updatePermissions"
/>
</q-scroll-area>
</q-card-section>
@ -143,3 +143,18 @@ export default defineComponent({
},
});
</script>
<style lang="sass" scoped>
// Same colors like qinput with filled attribute set
.body--light .background-like-input
background-color: rgba(0, 0, 0, 0.05)
&:hover
background: rgba(0,0,0,.1)
border-bottom: 1px solid rgba(0, 0, 0, 0.42)
.body--dark .background-like-input
background-color: rgba(255, 255, 255, 0.07)
&:hover
background: rgba(255, 255, 255, 0.14)
border-bottom: 1px solid #fff
</style>

View File

@ -1,5 +1,5 @@
<template>
<div>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab
v-for="(tabindex, index) in tabs"
@ -24,25 +24,23 @@
</q-item>
</q-list>
</q-drawer>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tab-panels
v-model="tab"
style="background-color: transparent"
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
animated
>
<q-tab-panel name="user">
<UpdateUser />
</q-tab-panel>
<q-tab-panel name="newUser">
<NewUser />
</q-tab-panel>
<q-tab-panel name="roles">
<RoleSettings v-if="canEditRoles" />
</q-tab-panel>
</q-tab-panels>
</q-page>
</div>
<q-tab-panels
v-model="tab"
style="background-color: transparent"
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
animated
>
<q-tab-panel name="user">
<UpdateUser />
</q-tab-panel>
<q-tab-panel name="newUser">
<NewUser />
</q-tab-panel>
<q-tab-panel name="roles">
<RoleSettings v-if="canEditRoles" />
</q-tab-panel>
</q-tab-panels>
</q-page>
</template>
<script lang="ts">

View File

@ -64,7 +64,7 @@ export const useUserStore = defineStore({
return data;
},
async uploadAvatar(user: FG.User, file: string) {
async uploadAvatar(user: FG.User, file: string | File) {
const formData = new FormData();
formData.append('file', file);
await api.post(`/users/${user.userid}/avatar`, formData, {

View File

@ -15,3 +15,9 @@ export function stringIsTime(val: string) {
export function stringIsDateTime(val: string) {
return !val || /^\d{4}-\d\d-\d\d \d\d:\d\d$/.test(val) || 'Datum und Zeit ist nicht gültig.';
}
export function isEmail(val: string) {
return (
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht gültig.'
);
}

841
yarn.lock

File diff suppressed because it is too large Load Diff