Compare commits

...

6 Commits

Author SHA1 Message Date
Tim Gröger 3f4f8a5fd4 update to version 1.1.0 2024-10-08 14:54:19 +02:00
Tim Gröger 668458e5e9 [prettier] 2024-10-08 14:44:52 +02:00
Tim Gröger e99d123156 [feat] sort nam eby displaymode setting 2024-10-08 14:31:43 +02:00
Tim Gröger cf82684ce4 [feat] add displaymode setting
user can choose, how users will be shown
2024-10-08 14:02:52 +02:00
Tim Gröger d6da01eeda [feat] filter user by name 2024-04-11 10:31:19 +02:00
Tim Gröger ed5bd72771 [feat] add view for all members 2024-01-24 13:29:14 +01:00
8 changed files with 322 additions and 6 deletions

View File

@ -1,6 +1,6 @@
{ {
"license": "MIT", "license": "MIT",
"version": "1.0.0", "version": "1.1.0",
"name": "@flaschengeist/users", "name": "@flaschengeist/users",
"author": "Ferdinand Thiessen <rpm@fthiessen.de>", "author": "Ferdinand Thiessen <rpm@fthiessen.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist", "homepage": "https://flaschengeist.dev/Flaschengeist",

View File

@ -0,0 +1,66 @@
<template>
<q-card>
<q-card-section class="text-h6"> Einstellungen Benutzer </q-card-section>
<q-card-section>
<q-select
v-model="displayNameMode"
:options="options"
label="Anzeige des Namens"
emit-value
map-options
input-debounce="0"
filled
/>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed } from 'vue';
import { useUserStore } from '@flaschengeist/api';
import { DisplayNameMode } from '../models';
export default defineComponent({
name: 'SettingWidget',
setup() {
const store = useUserStore();
onBeforeMount(() => {
void store.getDisplayNameModeSetting(true);
});
const displayNameMode = computed({
get: () => store.userSettings.display_name || DisplayNameMode.DISPLAYNAME,
set: (val) => {
console.log('set', val);
void store.setDisplayNameModeSetting(val);
},
});
const options = [
{
label: 'Anzeigename',
value: DisplayNameMode.DISPLAYNAME,
},
{
label: 'Vorname',
value: DisplayNameMode.FIRSTNAME,
},
{
label: 'Nachname',
value: DisplayNameMode.LASTNAME,
},
{
label: 'Vor- und Nachname',
value: DisplayNameMode.FIRSTNAME_LASTNAME,
},
{
label: 'Nachname, Vorname',
value: DisplayNameMode.LASTNAME_FIRSTNAME,
},
];
return {
displayNameMode,
options,
};
},
});
</script>

View File

@ -4,15 +4,19 @@
filled filled
:label="label" :label="label"
:options="users" :options="users"
option-label="display_name" :option-label="showName"
option-value="userid" option-value="userid"
map-options map-options
use-input
input-debounce="0"
@filter="filterFn"
/> />
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType, onBeforeMount } from 'vue'; import { computed, defineComponent, PropType, onBeforeMount, ref } from 'vue';
import { useUserStore } from '@flaschengeist/api'; import { useUserStore } from '@flaschengeist/api';
import { DisplayNameMode } from '../models';
export default defineComponent({ export default defineComponent({
name: 'UserSelector', name: 'UserSelector',
@ -26,17 +30,64 @@ export default defineComponent({
onBeforeMount(() => { onBeforeMount(() => {
void userStore.getUsers(false); void userStore.getUsers(false);
void userStore.getDisplayNameModeSetting(true);
}); });
const users = computed(() => userStore.users); const users = computed(() =>
userStore.users.filter((user) => {
let names = filter.value.toLowerCase().split(' ');
if (names.length < 1) {
return true;
}
if (names.length === 1) {
let name = names[0];
return (
user.lastname.toLowerCase().includes(name) ||
user.firstname.toLowerCase().includes(name)
);
}
if (names.length === 2) {
let name1 = names[0];
let name2 = names[1];
return (
(user.lastname.toLowerCase().includes(name1) &&
user.firstname.toLowerCase().includes(name2)) ||
(user.lastname.toLowerCase().includes(name2) &&
user.firstname.toLowerCase().includes(name1))
);
}
return true;
})
);
const filter = ref<string>('');
const filterFn = (val: string, update: () => void) => {
filter.value = val;
update();
};
const selected = computed({ const selected = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined), set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined),
}); });
function showName(user: FG.User) {
switch (userStore.userSettings.display_name) {
case DisplayNameMode.DISPLAYNAME:
return user.display_name;
case DisplayNameMode.FIRSTNAME:
return user.firstname;
case DisplayNameMode.LASTNAME:
return user.lastname;
case DisplayNameMode.FIRSTNAME_LASTNAME:
return `${user.firstname} ${user.lastname}`;
case DisplayNameMode.LASTNAME_FIRSTNAME:
return `${user.lastname}, ${user.firstname}`;
}
}
return { return {
selected, selected,
users, users,
filterFn,
showName,
}; };
}, },
}); });

View File

@ -1,6 +1,7 @@
import { FG_Plugin } from '@flaschengeist/types'; import { FG_Plugin } from '@flaschengeist/types';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import routes from './routes'; import routes from './routes';
import { DisplayNameMode } from './models';
const plugin: FG_Plugin.Plugin = { const plugin: FG_Plugin.Plugin = {
id: 'users', id: 'users',
@ -16,6 +17,15 @@ const plugin: FG_Plugin.Plugin = {
widget: defineAsyncComponent(() => import('./components/Widget.vue')), widget: defineAsyncComponent(() => import('./components/Widget.vue')),
}, },
], ],
settingWidgets: [
{
priority: 1,
name: 'userSettings',
permissions: [],
widget: defineAsyncComponent(() => import('./components/SettingWidget.vue')),
},
],
}; };
export default plugin; export default plugin;
export { DisplayNameMode };

View File

@ -12,3 +12,12 @@ export interface LoginResponse {
export interface CurrentUserResponse extends FG.User { export interface CurrentUserResponse extends FG.User {
permissions: FG.Permission[]; permissions: FG.Permission[];
} }
export enum DisplayNameMode {
FIRSTNAME = 'firstname',
LASTNAME = 'lastname',
FULLNAME = 'fullname',
DISPLAYNAME = 'display_name',
FIRSTNAME_LASTNAME = 'firstname_lastname',
LASTNAME_FIRSTNAME = 'lastname_firstname',
}

153
src/pages/Members.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<q-page>
<q-table
:columns="cols"
:rows="users"
:filter="filter"
:grid="grid"
:pagination="initialPagination"
>
<template v-slot:body-cell-avatar="props">
<q-td :key="props.key" :props="props">
<user-avatar v-model="props.row" />
</q-td>
</template>
<template v-slot:top-right="props">
<q-input v-model="filter" filled dense debounce="300" placeholder="Suche">
<template v-slot:append>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<q-btn
flat
round
dense
:icon="grid ? 'mdi-land-rows-horizontal' : 'mdi-card-account-details'"
@click="grid = !grid"
/>
</template>
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card bordered>
<div class="row justify-center">
<q-img :src="avatarURL(props.row.userid)">
<template #error>
<div
class="row fit justify-center items-center"
style="background-color: transparent"
>
<img src="no-image.svg" style="object-fit: contain; height: 100%" />
</div>
</template>
</q-img>
</div>
<q-card-section>
<div class="text-h6">{{ props.row.firstname }} {{ props.row.lastname }}</div>
<div class="text-caption">{{ props.row.display_name }}</div>
</q-card-section>
<q-card-section>
<div class="row items-center">
<q-btn
flat
dense
icon="mdi-email"
:href="'mailto:' + props.row.mail"
class="q-mr-xs"
no-caps
/>
<div class="text-caption">{{ props.row.mail }}</div>
</div>
<div class="row items-center">
<q-btn
flat
dense
icon="mdi-calendar"
class="q-mr-xs"
no-caps
v-if="props.row.birthday"
/>
<div class="text-caption" v-if="props.row.birthday">
{{ props.row.birthday.toLocaleDateString() }}
</div>
</div>
</q-card-section>
</q-card>
</div>
</template>
</q-table>
</q-page>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed, ref } from 'vue';
import { useUserStore, avatarURL } from '@flaschengeist/api';
import { UserAvatar } from '@flaschengeist/api/components';
const cols = [
{
name: 'avatar',
label: 'Profilbild',
sortable: false,
},
{
name: 'firstName',
label: 'Vorname',
field: 'firstname',
sortable: true,
},
{
name: 'lastName',
label: 'Nachname',
field: 'lastname',
sortable: true,
},
{
name: 'displayName',
label: 'Anzeigename',
field: 'display_name',
sortable: true,
},
{
name: 'mail',
label: 'E-Mail',
field: 'mail',
sortable: true,
},
{
name: 'birthday',
label: 'Geburtstag',
field: 'birthday',
sortable: true,
format: (val: Date | undefined) => {
return val?.toLocaleDateString();
},
},
];
export default defineComponent({
components: { UserAvatar },
setup() {
const userStore = useUserStore();
onBeforeMount(() => {
void userStore.getUsers(true);
});
const filter = ref('');
const users = computed(() => userStore.users);
const grid = ref(false);
return {
users,
cols,
filter,
grid,
avatarURL,
initialPagination: {
sortBy: 'lastName',
descending: false,
page: 1,
rowsPerPage: 0,
// rowsNumber: xx if getting data from a server
},
};
},
});
</script>

View File

@ -7,6 +7,9 @@
</q-card-section> </q-card-section>
<MainUserSettings :user="currentUser" @update:user="updateUser" /> <MainUserSettings :user="currentUser" @update:user="updateUser" />
</q-card> </q-card>
<div v-for="(item, index) in widgets" :key="index" class="full-height col-12">
<component :is="item.widget" />
</div>
<div class="col-12 text-left text-h6">Aktive Sessions:</div> <div class="col-12 text-left text-h6">Aktive Sessions:</div>
<user-session <user-session
v-for="(session, index) in sessions" v-for="(session, index) in sessions"
@ -14,15 +17,17 @@
v-model="sessions[index]" v-model="sessions[index]"
@delete="removeSession(session)" @delete="removeSession(session)"
/> />
<q-btn label="List Widgets" @click="listWidgets" />
</q-page> </q-page>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { useMainStore, useUserStore, useSessionStore } from '@flaschengeist/api'; import { useMainStore, useUserStore, useSessionStore, hasPermissions } from '@flaschengeist/api';
import MainUserSettings from '../components/settings/MainUserSettings.vue'; import MainUserSettings from '../components/settings/MainUserSettings.vue';
import { defineComponent, onBeforeMount, ref } from 'vue'; import { defineComponent, onBeforeMount, ref, computed, inject } from 'vue';
import UserSession from '../components/settings/UserSession.vue'; import UserSession from '../components/settings/UserSession.vue';
import { FG_Plugin } from '@flaschengeist/types';
import { Notify } from 'quasar'; import { Notify } from 'quasar';
export default defineComponent({ export default defineComponent({
@ -47,17 +52,29 @@ export default defineComponent({
progress: true, progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }], actions: [{ icon: 'mdi-close', color: 'white' }],
}); });
console.log(widgets);
} }
function removeSession(s: FG.Session) { function removeSession(s: FG.Session) {
sessions.value = sessions.value.filter((ss) => ss.token !== s.token); sessions.value = sessions.value.filter((ss) => ss.token !== s.token);
} }
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
const widgets = computed(() =>
flaschengeist?.settingWidgets.filter((widget) => hasPermissions(widget.permissions))
);
function listWidgets() {
console.log(widgets);
}
return { return {
currentUser, currentUser,
sessions, sessions,
updateUser, updateUser,
removeSession, removeSession,
widgets,
listWidgets,
}; };
}, },
}); });

View File

@ -10,6 +10,16 @@ const mainRoutes: FG_Plugin.MenuRoute[] = [
permissions: ['user'], permissions: ['user'],
route: { path: 'user', name: 'user', redirect: { name: 'user-settings' } }, route: { path: 'user', name: 'user', redirect: { name: 'user-settings' } },
children: [ children: [
{
title: 'Mitglieder',
icon: 'mdi-account-multiple',
shortcut: true,
route: {
path: 'members',
name: 'user-members',
component: () => import('../pages/Members.vue'),
},
},
{ {
title: 'Einstellungen', title: 'Einstellungen',
icon: 'mdi-account-edit', icon: 'mdi-account-edit',