Compare commits
95 Commits
feature/ba
...
develop
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | e8c0001d17 | |
Ferdinand Thiessen | 5c8637965a | |
Ferdinand Thiessen | 308d348755 | |
Ferdinand Thiessen | 2425e6cf2f | |
Ferdinand Thiessen | a9edc12494 | |
Ferdinand Thiessen | 1525de1469 | |
Ferdinand Thiessen | 6a75d1bf51 | |
Ferdinand Thiessen | f9f66e7172 | |
Ferdinand Thiessen | e9c0086859 | |
Ferdinand Thiessen | b2c70a6657 | |
Ferdinand Thiessen | f27212f60e | |
Ferdinand Thiessen | 9eb5412c14 | |
Ferdinand Thiessen | 8e552ba508 | |
Ferdinand Thiessen | 49c3ec74ba | |
Ferdinand Thiessen | 83f32ea82a | |
Ferdinand Thiessen | 656d7a9e3c | |
Ferdinand Thiessen | 8fca175d39 | |
Ferdinand Thiessen | 6c219c5226 | |
Ferdinand Thiessen | cb43b8a39b | |
Tim Gröger | acf1816b55 | |
Ferdinand Thiessen | 02f60335d4 | |
Ferdinand Thiessen | d9267bcc0a | |
Ferdinand Thiessen | 6732921ff7 | |
Ferdinand Thiessen | 7a705d5f9a | |
Ferdinand Thiessen | 368ca23c56 | |
Tim Gröger | d82e025700 | |
Tim Gröger | 07e1966471 | |
Ferdinand Thiessen | 29c085bd2c | |
Ferdinand Thiessen | 1b152b52f5 | |
Ferdinand Thiessen | 6769e18ffa | |
Ferdinand Thiessen | 88dd96c937 | |
Ferdinand Thiessen | 4887bc261b | |
Ferdinand Thiessen | d516839ad4 | |
Ferdinand Thiessen | c9df257bbf | |
Tim Gröger | 053fdae384 | |
Ferdinand Thiessen | 6fd3f045f8 | |
Ferdinand Thiessen | 2d167ebbae | |
Ferdinand Thiessen | a8578e2803 | |
Ferdinand Thiessen | e4889ddac2 | |
Ferdinand Thiessen | 664def40fc | |
Ferdinand Thiessen | ade6d06eb6 | |
Tim Gröger | 53f053a294 | |
Tim Gröger | cc29893e04 | |
Ferdinand Thiessen | 42800a9d99 | |
Ferdinand Thiessen | f4650ffdeb | |
Ferdinand Thiessen | 1158525abb | |
Ferdinand Thiessen | 04e3c57397 | |
Tim Gröger | 2fc411d51d | |
Tim Gröger | 6061b37887 | |
Tim Gröger | efde9a2ee7 | |
Tim Gröger | e96d15bc66 | |
Tim Gröger | dad88ec766 | |
Ferdinand Thiessen | 3e091fd02b | |
Ferdinand Thiessen | fca79c36ef | |
Ferdinand Thiessen | 34fcdbdb7f | |
Ferdinand Thiessen | d84796b09d | |
Ferdinand Thiessen | 62af4f5026 | |
Tim Gröger | 36d6fdfb94 | |
Ferdinand Thiessen | a30da50b1d | |
Ferdinand Thiessen | 53951daa25 | |
Ferdinand Thiessen | 59920e23a5 | |
Ferdinand Thiessen | 5952c9b7f2 | |
Ferdinand Thiessen | 73f50e9f4f | |
Ferdinand Thiessen | dfb924bb3f | |
Ferdinand Thiessen | bc9dba1c7b | |
Ferdinand Thiessen | 1132bfd129 | |
Ferdinand Thiessen | fe1fae10f5 | |
Ferdinand Thiessen | 1c36399ac0 | |
Tim Gröger | a38954cf70 | |
Ferdinand Thiessen | 35d8433e23 | |
groegert | 731cc20a06 | |
groegert | c38032085f | |
Ferdinand Thiessen | 22f47bd34e | |
Tim Gröger | 16fd9201ae | |
Ferdinand Thiessen | 873fee3301 | |
Ferdinand Thiessen | 1802081ad2 | |
Ferdinand Thiessen | 631e78acb3 | |
Ferdinand Thiessen | d422380adc | |
Ferdinand Thiessen | 0c279289b2 | |
Ferdinand Thiessen | 979eab05af | |
Ferdinand Thiessen | fd918f5bb7 | |
Ferdinand Thiessen | 068dbdcc7b | |
Ferdinand Thiessen | 8c9db67b95 | |
groegert | 86bad83e53 | |
groegert | 625ac55b0a | |
groegert | 9940589d1a | |
groegert | f2b7f3a3b4 | |
Ferdinand Thiessen | f9c9f6efbe | |
Ferdinand Thiessen | 0873b2da22 | |
groegert | 734a3e51c9 | |
Ferdinand Thiessen | cf1a5cc922 | |
Ferdinand Thiessen | 6e503ed38f | |
Ferdinand Thiessen | 4cbff6b077 | |
Ferdinand Thiessen | 2b42dad617 | |
Ferdinand Thiessen | 8b6c400689 |
31
.eslintrc.js
|
@ -17,11 +17,11 @@ module.exports = {
|
||||||
project: resolve(__dirname, './tsconfig.json'),
|
project: resolve(__dirname, './tsconfig.json'),
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
||||||
sourceType: 'module' // Allows for the use of imports
|
sourceType: 'module', // Allows for the use of imports
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
browser: true
|
browser: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rules order is important, please avoid shuffling them
|
// Rules order is important, please avoid shuffling them
|
||||||
|
@ -44,7 +44,7 @@ module.exports = {
|
||||||
|
|
||||||
// https://github.com/prettier/eslint-config-prettier#installation
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
'prettier', //'plugin:prettier/recommended'
|
'plugin:prettier/recommended',
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -54,10 +54,6 @@ module.exports = {
|
||||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
'vue',
|
'vue',
|
||||||
|
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
|
||||||
// Prettier has not been included as plugin to avoid performance impact
|
|
||||||
// add it as an extension for your IDE
|
|
||||||
],
|
],
|
||||||
|
|
||||||
globals: {
|
globals: {
|
||||||
|
@ -70,19 +66,26 @@ module.exports = {
|
||||||
__QUASAR_SSR_PWA__: true,
|
__QUASAR_SSR_PWA__: true,
|
||||||
process: true,
|
process: true,
|
||||||
Capacitor: true,
|
Capacitor: true,
|
||||||
chrome: true
|
chrome: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
rules: {
|
||||||
'prefer-promise-reject-errors': 'off',
|
// VueStuff
|
||||||
|
// Defaults to error on eslint-plugin-vue 8.0.3, but let us be not too strict with names
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
|
||||||
// TypeScript
|
// Rejects on promises should always be of the Error type (and allow empty rejects as well)
|
||||||
quotes: ['warn', 'single', { avoidEscape: true }],
|
'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
|
||||||
|
|
||||||
|
// Allow " if ' is contained inside the string, so we can avoid escaping
|
||||||
|
quotes: ['error', 'single', { avoidEscape: true }],
|
||||||
|
|
||||||
|
// TypeScript, let us be not too strict
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
|
||||||
// allow debugger during development only
|
// allow debugger during development only
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ node_modules
|
||||||
|
|
||||||
# We use yarn, so ignore npm
|
# We use yarn, so ignore npm
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# Quasar core related directories
|
# Quasar core related directories
|
||||||
.quasar
|
.quasar
|
||||||
|
@ -17,6 +18,8 @@ package-lock.json
|
||||||
|
|
||||||
# Capacitor related directories and files
|
# Capacitor related directories and files
|
||||||
/src-capacitor/www
|
/src-capacitor/www
|
||||||
|
/src-capacitor/android
|
||||||
|
/src-capacitor/ios
|
||||||
/src-capacitor/node_modules
|
/src-capacitor/node_modules
|
||||||
|
|
||||||
# BEX related directories and files
|
# BEX related directories and files
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
[submodule "deps/quasar-ui-qcalendar"]
|
|
||||||
path = deps/quasar-ui-qcalendar
|
|
||||||
url = https://github.com/susnux/quasar-ui-qcalendar
|
|
||||||
branch = quasar2
|
|
|
@ -3,6 +3,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
// to edit target browsers: use "browserslist" field in package.json
|
// to edit target browsers: use "browserslist" field in package.json
|
||||||
require('autoprefixer')
|
require('autoprefixer'),
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
pipeline:
|
||||||
|
install:
|
||||||
|
image: node:lts-alpine
|
||||||
|
commands:
|
||||||
|
- yarn install
|
||||||
|
lint:
|
||||||
|
image: node:lts-alpine
|
||||||
|
commands:
|
||||||
|
- yarn lint
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: node:lts-alpine
|
||||||
|
commands:
|
||||||
|
- yarn quasar build
|
||||||
|
|
||||||
|
branches: [main, develop]
|
106
README.md
|
@ -1,62 +1,88 @@
|
||||||
# Flaschengeist (frontend)
|
# Flaschengeist (frontend)
|
||||||
|
|
||||||
|
![status-badge](http://os-sc.org:8000/api/badges/ferfissimo/flaschengeist-frontend/status.svg)
|
||||||
|
|
||||||
Modular student club administration system, licensed under the MIT license.
|
Modular student club administration system, licensed under the MIT license.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
```
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.1",
|
||||||
|
"npm": ">= 6.14.12",
|
||||||
|
"yarn": ">= 1.22.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So on debian (buster and bullseye) you will need to install node.js and yarn beside the debian packages to meet the needed versions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pushd ~/opt
|
||||||
|
wget https://nodejs.org/dist/latest-v16.x/node-v16.13.0-linux-x64.tar.xz
|
||||||
|
tar -xJf node-v16.13.0-linux-x64.tar.xz
|
||||||
|
export PATH="$(pwd)/node-v16.13.0-linux-x64/bin":"$PATH"
|
||||||
|
npm i -g yarn
|
||||||
|
npm i -g @quasar/cli
|
||||||
|
popd
|
||||||
|
```
|
||||||
|
|
||||||
### Install the dependencies
|
### Install the dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Be aware npm might not work.
|
||||||
|
|
||||||
### Configure Plugins
|
### Configure Plugins
|
||||||
|
|
||||||
You can activate and deactive Plugins in `src/boot/plugins.ts`.
|
#### Installing a plugin
|
||||||
You have to set the name of the Plugin into `config.loadModules`.
|
|
||||||
|
Simply add it as a dependency and install it, for example installing the `pricelist`-plugin:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add '@flaschengeist/pricelist'
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Enable / Disable a plugin
|
||||||
|
|
||||||
|
After installing a plugin you will have to enable it,
|
||||||
|
this is done by adding it to the `plugin.config.js` file.
|
||||||
|
For the example above the file should look like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = [
|
||||||
|
// pricelist plugin:
|
||||||
|
'@flaschengeist/pricelist',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to rebuild the project
|
||||||
|
|
||||||
|
### Configure Backend
|
||||||
|
|
||||||
|
The application is using the API of [the backend](https://flaschengeist.dev/Flaschengeist/flaschengeist)
|
||||||
|
This access needs to be configured in `src/config.ts'->config.baseURL
|
||||||
|
|
||||||
|
- either you do have a proxy webserver that maps the '/api' to the backend (http://localhost:5000) or
|
||||||
|
- you do directly configure the backend there:`baseURL: 'http://localhost:5000'`. Be aware not committing this configuration.
|
||||||
|
|
||||||
### Build the application
|
### Build the application
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
yarn quasar build
|
yarn quasar build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Notes on mobile apps (Cordova)
|
||||||
|
|
||||||
|
For mobile applications older web engines should or must be supported,
|
||||||
|
as manufaturer often do not update their phones, so for building cordova apps set the `BROWSERSLIST_ENV` environment variable to
|
||||||
|
`BROWSERSLIST_ENV=cordova`.
|
||||||
|
This will produce ECDMAscript compatible with iOS 13+ and Android Webview 76 (relased October 2019).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Icons used
|
Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
|
||||||
|
|
||||||
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[];
|
|
||||||
requiredModules: string[];
|
|
||||||
version: string;
|
|
||||||
```
|
|
||||||
|
|
||||||
You have to import `FG_Plugin` from `plugins.d.ts`.
|
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { date as q_date } from 'quasar';
|
import { date as q_date } from 'quasar';
|
||||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from 'src/utils/validators';
|
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'IsoDateInput',
|
name: 'IsoDateInput',
|
||||||
|
@ -54,7 +54,7 @@ export default defineComponent({
|
||||||
label: { type: String, default: 'Datum' },
|
label: { type: String, default: 'Datum' },
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
rules: {
|
rules: {
|
||||||
type: Array as PropType<Validator[]>,
|
type: Array as PropType<Validator<Date>[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,22 @@ export default defineComponent({
|
||||||
setup(props, { emit, attrs }) {
|
setup(props, { emit, attrs }) {
|
||||||
const customRules = computed(() => [
|
const customRules = computed(() => [
|
||||||
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
|
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
|
||||||
...props.rules,
|
(value?: string) => {
|
||||||
|
if (props.rules.length > 0 && !!value) {
|
||||||
|
let date: Date | undefined = undefined;
|
||||||
|
if (props.type == 'date') date = modifyDate(value);
|
||||||
|
else if (props.type == 'time') date = modifyTime(value);
|
||||||
|
else {
|
||||||
|
const split = value.split(' ');
|
||||||
|
date = modifyTime(split[1], modifyDate(split[0]));
|
||||||
|
}
|
||||||
|
for (const rule of props.rules) {
|
||||||
|
const r = rule(date);
|
||||||
|
if (typeof r === 'string') return r;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const clearable = computed(() =>
|
const clearable = computed(() =>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<q-avatar>
|
||||||
|
<slot :avatarURL="avatarURL(modelValue)">
|
||||||
|
<q-img :src="avatarURL(modelValue)" style="min-width: 100%; min-height: 100%">
|
||||||
|
<template #error>
|
||||||
|
<img :src="fallback" style="height: 100%" />
|
||||||
|
</template>
|
||||||
|
</q-img>
|
||||||
|
</slot>
|
||||||
|
</q-avatar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import { avatarURL } from '@flaschengeist/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an avatar for an user
|
||||||
|
*
|
||||||
|
* Slots:
|
||||||
|
* default - scope: {avatarURL}
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'UserAvatar',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [Object, String] as PropType<FG.User | string>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showZoom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
type: String,
|
||||||
|
default: 'no-image.svg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['error'],
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
avatarURL,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,5 @@
|
||||||
|
import IsoDateInput from './IsoDateInput.vue';
|
||||||
|
import PasswordInput from './PasswordInput.vue';
|
||||||
|
import UserAvatar from './UserAvatar.vue';
|
||||||
|
|
||||||
|
export { IsoDateInput, PasswordInput, UserAvatar };
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { api, pinia } from './src/internal';
|
||||||
|
|
||||||
|
export * from './src/stores/';
|
||||||
|
|
||||||
|
export * from './src/utils/datetime';
|
||||||
|
export * from './src/utils/permission';
|
||||||
|
export * from './src/utils/persistent';
|
||||||
|
export * from './src/utils/user';
|
||||||
|
export * from './src/utils/validators';
|
||||||
|
export * from './src/utils/misc';
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "1.0.0-alpha.7",
|
||||||
|
"name": "@flaschengeist/api",
|
||||||
|
"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/issues"
|
||||||
|
},
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@quasar/app": "^3.2.4",
|
||||||
|
"flaschengeist": "^2.0.0-alpha.1",
|
||||||
|
"pinia": "^2.0.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flaschengeist/types": "^1.0.0-alpha.10",
|
||||||
|
"@types/node": "^14.18.00",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
//https://github.com/vuejs/vue-next/issues/3130
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { ComponentOptions } from 'vue';
|
||||||
|
const component: ComponentOptions;
|
||||||
|
export default component;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
export const api = axios.create();
|
||||||
|
|
||||||
|
export const pinia = createPinia();
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if error is an AxiosError, and optional if a specific status was returned
|
||||||
|
*
|
||||||
|
* @param error Thrown error to check
|
||||||
|
* @param status If set, check if this error has set thouse status code
|
||||||
|
*/
|
||||||
|
export function isAxiosError(error: unknown, status?: number) {
|
||||||
|
// Check if it is an axios error (with axios 1.0 `error instanceof AxiosError` will be possible)
|
||||||
|
if (typeof error !== 'object' || !error || !('isAxiosError' in error)) return false;
|
||||||
|
// Check status code if status was given
|
||||||
|
if (status !== undefined)
|
||||||
|
return (
|
||||||
|
(<AxiosError>error).response !== undefined && (<AxiosError>error).response?.status === status
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './main';
|
||||||
|
export * from './session';
|
||||||
|
export * from './user';
|
|
@ -1,42 +1,48 @@
|
||||||
import { useUserStore, useSessionStore } from 'src/plugins/user/store';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import { translateNotification } from 'src/boot/plugins';
|
import { fixSession, useSessionStore, useUserStore } from '.';
|
||||||
import { LocalStorage, SessionStorage } from 'quasar';
|
|
||||||
import { FG_Plugin } from 'src/plugins';
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from '../internal';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import { PersistentStorage } from '../utils/persistent';
|
||||||
|
|
||||||
function loadCurrentSession() {
|
function reviveSession() {
|
||||||
const session = LocalStorage.getItem<FG.Session>('session');
|
return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
|
||||||
if (session) session.expires = new Date(session.expires);
|
|
||||||
return session || undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUser() {
|
function clearPersistant() {
|
||||||
const user = SessionStorage.getItem<FG.User>('user');
|
void PersistentStorage.remove('fg_session');
|
||||||
if (user && user.birthday) user.birthday = new Date(user.birthday);
|
}
|
||||||
return user || undefined;
|
|
||||||
|
export function saveSession(session?: FG.Session) {
|
||||||
|
if (session === undefined) return clearPersistant();
|
||||||
|
PersistentStorage.set('fg_session', session).catch(() =>
|
||||||
|
console.error('Could not save token to storage')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMainStore = defineStore({
|
export const useMainStore = defineStore({
|
||||||
id: 'main',
|
id: 'main',
|
||||||
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
session: loadCurrentSession(),
|
session: undefined as FG.Session | undefined,
|
||||||
user: loadUser(),
|
user: undefined as FG.User | undefined,
|
||||||
notifications: [] as Array<FG_Plugin.Notification>,
|
notifications: [] as Array<FG_Plugin.Notification>,
|
||||||
shortcuts: [] as FG_Plugin.MenuLink[],
|
shortcuts: [] as Array<FG_Plugin.MenuLink>,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
loggedIn() {
|
loggedIn(): boolean {
|
||||||
return this.session !== undefined;
|
return this.session !== undefined;
|
||||||
},
|
},
|
||||||
currentUser() {
|
currentUser(): FG.User {
|
||||||
if (this.user === undefined) throw 'Not logged in, this should not be called';
|
if (this.user === undefined) throw 'Not logged in, this should not be called';
|
||||||
return this.user;
|
return this.user;
|
||||||
},
|
},
|
||||||
permissions() {
|
currentSession(): FG.Session {
|
||||||
|
if (this.session === undefined) throw 'Not logged in, this should not be called';
|
||||||
|
return this.session;
|
||||||
|
},
|
||||||
|
permissions(): string[] {
|
||||||
return this.user?.permissions || [];
|
return this.user?.permissions || [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -46,29 +52,29 @@ export const useMainStore = defineStore({
|
||||||
* Updates session and loads current user
|
* Updates session and loads current user
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
if (this.session) {
|
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const session = await sessionStore.getSession(this.session.token);
|
|
||||||
if (session) {
|
|
||||||
this.session = this.session;
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = await userStore.getUser(this.session.userid);
|
|
||||||
if (user) {
|
try {
|
||||||
this.user = user;
|
this.session = await reviveSession();
|
||||||
SessionStorage.set('user', user);
|
if (this.session !== undefined) {
|
||||||
}
|
this.session = await sessionStore.getSession(this.session.token);
|
||||||
|
if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not load token from storage', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async login(userid: string, password: string) {
|
async login(userid: string, password: string) {
|
||||||
|
const userStore = useUserStore();
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post<FG.Session>('/auth', { userid, password });
|
const { data } = await api.post<FG.Session>('/auth', { userid, password });
|
||||||
this.session = data;
|
this.session = fixSession(data);
|
||||||
this.session.expires = new Date(this.session.expires);
|
this.user = await userStore.getUser(data.userid, true);
|
||||||
LocalStorage.set('session', this.session);
|
|
||||||
return true;
|
return true;
|
||||||
} catch ({ response }) {
|
} catch ({ response }) {
|
||||||
|
this.handleLoggedOut();
|
||||||
return (<AxiosResponse | undefined>response)?.status || false;
|
return (<AxiosResponse | undefined>response)?.status || false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,19 +82,13 @@ export const useMainStore = defineStore({
|
||||||
async logout() {
|
async logout() {
|
||||||
if (!this.session || !this.session.token) return false;
|
if (!this.session || !this.session.token) return false;
|
||||||
|
|
||||||
LocalStorage.clear();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = this.session.token;
|
const token = this.session.token;
|
||||||
this.$patch({
|
|
||||||
session: undefined,
|
|
||||||
user: undefined,
|
|
||||||
});
|
|
||||||
await api.delete(`/auth/${token}`);
|
await api.delete(`/auth/${token}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
SessionStorage.clear();
|
this.handleLoggedOut();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
@ -110,23 +110,22 @@ export const useMainStore = defineStore({
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
|
async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
|
||||||
const params =
|
const { data } = await api.get<FG.Notification[]>('/notifications', {
|
||||||
|
params:
|
||||||
this.notifications.length > 0
|
this.notifications.length > 0
|
||||||
? { from: this.notifications[this.notifications.length - 1].time }
|
? { from: this.notifications[this.notifications.length - 1].time }
|
||||||
: {};
|
: {},
|
||||||
const { data } = await api.get<FG.Notification[]>('/notifications', { params: params });
|
});
|
||||||
const notifications: FG_Plugin.Notification[] = [];
|
|
||||||
|
const notes = [] as FG_Plugin.Notification[];
|
||||||
data.forEach((n) => {
|
data.forEach((n) => {
|
||||||
n.time = new Date(n.time);
|
n.time = new Date(n.time);
|
||||||
notifications.push(
|
const plugin = flaschengeist?.plugins.filter((p) => p.id === n.plugin)[0];
|
||||||
(
|
if (!plugin) console.debug('Could not find a parser for this notification', n);
|
||||||
flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification ||
|
else notes.push(plugin.notification(n));
|
||||||
translateNotification
|
|
||||||
)(n)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
this.notifications.push(...notifications);
|
this.notifications.push(...notes);
|
||||||
return notifications;
|
return notes;
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeNotification(id: number) {
|
async removeNotification(id: number) {
|
||||||
|
@ -151,6 +150,11 @@ export const useMainStore = defineStore({
|
||||||
async setShortcuts() {
|
async setShortcuts() {
|
||||||
await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
|
await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleLoggedOut() {
|
||||||
|
this.$reset();
|
||||||
|
void clearPersistant();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from '../internal';
|
||||||
|
import { isAxiosError, useMainStore } from '.';
|
||||||
|
|
||||||
|
export function fixSession(s?: FG.Session) {
|
||||||
|
return !s ? s : Object.assign(s, { expires: new Date(s.expires) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSessionStore = defineStore({
|
||||||
|
id: 'sessions',
|
||||||
|
|
||||||
|
state: () => ({}),
|
||||||
|
|
||||||
|
getters: {},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getSession(token: string) {
|
||||||
|
return await api
|
||||||
|
.get(`/auth/${token}`)
|
||||||
|
.then(({ data }: AxiosResponse<FG.Session>) => data)
|
||||||
|
.catch(() => undefined);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSessions() {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.Session[]>('/auth');
|
||||||
|
data.forEach(fixSession);
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
const currentSession = data.find((session) => {
|
||||||
|
return session.token === mainStore.session?.token;
|
||||||
|
});
|
||||||
|
if (currentSession) {
|
||||||
|
mainStore.session = currentSession;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
return [] as FG.Session[];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSession(token: string) {
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
if (token === mainStore.session?.token) return mainStore.logout();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/auth/${token}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore 401, as this means we are already logged out, throw all other
|
||||||
|
if (!isAxiosError(error, 401)) throw error;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateSession(lifetime: number, token: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
|
||||||
|
fixSession(data);
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
if (mainStore.session?.token == data.token) mainStore.session = data;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from '../internal';
|
||||||
|
import { isAxiosError, useMainStore } from '.';
|
||||||
|
|
||||||
|
export function fixUser(u?: FG.User) {
|
||||||
|
return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if state is outdated / dirty
|
||||||
|
* Value is considered outdated after 15 minutes
|
||||||
|
* @param updated Time of last updated (in milliseconds see Date.now())
|
||||||
|
* @returns True if outdated, false otherwise
|
||||||
|
*/
|
||||||
|
function isDirty(updated: number) {
|
||||||
|
return Date.now() - updated > 15 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = defineStore({
|
||||||
|
id: 'users',
|
||||||
|
|
||||||
|
state: () => ({
|
||||||
|
roles: [] as FG.Role[],
|
||||||
|
permissions: [] as FG.Permission[],
|
||||||
|
// list of all users, include deleted ones, use `users` getter for list of active ones
|
||||||
|
_users: [] as FG.User[],
|
||||||
|
// Internal flags for deciding if lists need to force-loaded
|
||||||
|
_dirty_users: 0,
|
||||||
|
_dirty_roles: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
users(state) {
|
||||||
|
return state._users.filter((u) => !u.deleted);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/** Simply filter all users by ID */
|
||||||
|
findUser(userid: string) {
|
||||||
|
return this._users.find((user) => user.userid === userid);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Retrieve user by ID
|
||||||
|
* @param userid ID of user to retrieve
|
||||||
|
* @param force If set to true the user is loaded from backend even when a local copy is available
|
||||||
|
* @returns Retrieved user (Promise) or raise an error
|
||||||
|
* @throws Probably an AxiosError if loading failed
|
||||||
|
*/
|
||||||
|
async getUser(userid: string, force = false) {
|
||||||
|
const idx = this._users.findIndex((user) => user.userid === userid);
|
||||||
|
if (force || idx === -1 || isDirty(this._dirty_users)) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.User>(`/users/${userid}`);
|
||||||
|
fixUser(data);
|
||||||
|
if (idx === -1) this._users.push(data);
|
||||||
|
else this._users[idx] = data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore 404, throw all other
|
||||||
|
if (!isAxiosError(error, 404)) throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this._users[idx];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Retrieve list of all users
|
||||||
|
* @param force If set to true a fresh users list is loaded from backend even when a local copy is available
|
||||||
|
* @returns Array of retrieved users (Promise)
|
||||||
|
* @throws Probably an AxiosError if loading failed
|
||||||
|
*/
|
||||||
|
async getUsers(force = false) {
|
||||||
|
if (force || isDirty(this._dirty_users)) {
|
||||||
|
const { data } = await api.get<FG.User[]>('/users');
|
||||||
|
data.forEach(fixUser);
|
||||||
|
this._users = data;
|
||||||
|
this._dirty_users = Date.now();
|
||||||
|
}
|
||||||
|
return this._users;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Save modifications of user on backend
|
||||||
|
* @param user Modified user to save
|
||||||
|
* @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
|
||||||
|
*/
|
||||||
|
async updateUser(user: FG.User) {
|
||||||
|
await api.put(`/users/${user.userid}`, user);
|
||||||
|
// Modifcation accepted by backend
|
||||||
|
// Save modifications back to our users list
|
||||||
|
const idx = this._users.findIndex((u) => u.userid === user.userid);
|
||||||
|
if (idx > -1) this._users[idx] = user;
|
||||||
|
// If user was current user, save modifications back to the main store
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
if (user.userid === mainStore.user?.userid) mainStore.user = user;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Register a new user
|
||||||
|
* @param user User to register (id not set)
|
||||||
|
* @returns The registered user (id set)
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async createUser(user: FG.User) {
|
||||||
|
const { data } = await api.post<FG.User>('/users', user);
|
||||||
|
this._users.push(<FG.User>fixUser(data));
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Delete an user
|
||||||
|
* Throws if failed and resolves void if succeed
|
||||||
|
*
|
||||||
|
* @param user User or ID of user to delete
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async deleteUser(user: FG.User | string) {
|
||||||
|
if (typeof user === 'object') user = user.userid;
|
||||||
|
|
||||||
|
await api.delete(`/users/${user}`);
|
||||||
|
this._users = this._users.filter((u) => u.userid != user);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Upload an avatar for an user
|
||||||
|
* Throws if failed and resolves void if succeed
|
||||||
|
*
|
||||||
|
* @param user User or ID of user
|
||||||
|
* @param file Avatar file to upload
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async uploadAvatar(user: FG.User | string, file: string | File) {
|
||||||
|
if (typeof user === 'object') user = user.userid;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
await api.post(`/users/${user}/avatar`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Delete avatar of an user
|
||||||
|
* @param user User or ID of user
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async deleteAvatar(user: FG.User | string) {
|
||||||
|
if (typeof user === 'object') user = user.userid;
|
||||||
|
|
||||||
|
await api.delete(`/users/${user}/avatar`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Retrieve list of all permissions
|
||||||
|
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
||||||
|
* @returns Array of retrieved permissions (Promise)
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async getPermissions(force = false) {
|
||||||
|
if (force || this.permissions.length === 0) {
|
||||||
|
const { data } = await api.get<FG.Permission[]>('/roles/permissions');
|
||||||
|
this.permissions = data;
|
||||||
|
}
|
||||||
|
return this.permissions;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Retrieve list of all roles
|
||||||
|
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
||||||
|
* @returns Array of retrieved roles (Promise)
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async getRoles(force = false) {
|
||||||
|
if (force || isDirty(this._dirty_roles)) {
|
||||||
|
const { data } = await api.get<FG.Role[]>('/roles');
|
||||||
|
this.roles = data;
|
||||||
|
this._dirty_roles = Date.now();
|
||||||
|
}
|
||||||
|
return this.roles;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Save modifications of role on the backend
|
||||||
|
* @param role role to save
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async updateRole(role: FG.Role) {
|
||||||
|
await api.put(`/roles/${role.id}`, role);
|
||||||
|
|
||||||
|
const idx = this.roles.findIndex((r) => r.id === role.id);
|
||||||
|
if (idx != -1) this.roles[idx] = role;
|
||||||
|
else this._dirty_roles = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Create a new role
|
||||||
|
* @param role Role to create (ID not set)
|
||||||
|
* @returns Created role (ID set)
|
||||||
|
* @throws Probably an AxiosError if request failed
|
||||||
|
*/
|
||||||
|
async newRole(role: FG.Role) {
|
||||||
|
const { data } = await api.post<FG.Role>('/roles', role);
|
||||||
|
this.roles.push(data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Delete a role
|
||||||
|
* @param role Role or ID of role to delete
|
||||||
|
* @throws Probably an AxiosError if request failed (409 if role still in use)
|
||||||
|
*/
|
||||||
|
async deleteRole(role: FG.Role | number) {
|
||||||
|
if (typeof role === 'object') role = role.id;
|
||||||
|
await api.delete(`/roles/${role}`);
|
||||||
|
this.roles = this.roles.filter((r) => r.id !== role);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -17,12 +17,23 @@ export function formatDateTime(
|
||||||
return dateTimeFormat.format(date);
|
return dateTimeFormat.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asDate(date?: Date) {
|
export function asDate(date?: Date, placeholder = '') {
|
||||||
return date ? formatDateTime(date, true) : '';
|
return date ? formatDateTime(date, true) : placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asHour(date?: Date) {
|
export function asHour(date?: Date, placeholder = '') {
|
||||||
return date ? formatDateTime(date, false, true) : '';
|
return date ? formatDateTime(date, false, true) : placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatStartEnd(start: Date, end?: Date) {
|
||||||
|
const today = asDate(new Date());
|
||||||
|
const startDate = asDate(start);
|
||||||
|
const endDate = end ? asDate(end) : '';
|
||||||
|
return (
|
||||||
|
(today !== startDate ? `${startDate}, ` : '') +
|
||||||
|
asHour(start) +
|
||||||
|
(end ? ' - ' + (endDate !== startDate ? `${endDate}, ` : '') + asHour(end) : '')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startOfWeek(date: Date, startMonday = true) {
|
export function startOfWeek(date: Date, startMonday = true) {
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function clone<T>(o: T): T {
|
||||||
|
return <T>JSON.parse(JSON.stringify(o));
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMainStore } from 'src/stores';
|
import { useMainStore } from '../stores';
|
||||||
|
|
||||||
export function hasPermission(permission: string) {
|
export function hasPermission(permission: string) {
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { LocalStorage, Platform } from 'quasar';
|
||||||
|
import { Storage } from '@capacitor/storage';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type PersitentTypes = Date | RegExp | number | boolean | string | object;
|
||||||
|
|
||||||
|
export class PersistentStorage {
|
||||||
|
static clear() {
|
||||||
|
if (Platform.is.capacitor) return Storage.clear();
|
||||||
|
else return Promise.resolve(LocalStorage.clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(key: string) {
|
||||||
|
if (Platform.is.capacitor) return Storage.remove({ key: key });
|
||||||
|
else return Promise.resolve(LocalStorage.remove(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
static set(key: string, value: PersitentTypes) {
|
||||||
|
if (Platform.is.capacitor) return Storage.set({ key, value: JSON.stringify(value) });
|
||||||
|
else return Promise.resolve(LocalStorage.set(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static get<T extends PersitentTypes>(key: string) {
|
||||||
|
if (Platform.is.capacitor)
|
||||||
|
return Storage.get({ key }).then((v) =>
|
||||||
|
v.value === null ? null : (JSON.parse(v.value) as T)
|
||||||
|
);
|
||||||
|
else return Promise.resolve(LocalStorage.getItem<T>(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
static keys() {
|
||||||
|
if (Platform.is.capacitor) return Storage.keys().then((v) => v.keys);
|
||||||
|
else return Promise.resolve(LocalStorage.getAllKeys());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { api } from '../internal';
|
||||||
|
|
||||||
|
export function avatarURL(user: FG.User | string, thumbnail = true) {
|
||||||
|
if (typeof user === 'object') user = user.userid;
|
||||||
|
return `${api.defaults?.baseURL || ''}/users/${user}/avatar${thumbnail ? '?thumbnail' : ''}`;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export type Validator = (value: unknown) => boolean | string;
|
export type Validator<T = unknown> = (value?: T | null) => boolean | string;
|
||||||
|
|
||||||
export function notEmpty(val: unknown) {
|
export function notEmpty(val: unknown) {
|
||||||
return !!val || 'Feld darf nicht leer sein!';
|
return !!val || 'Feld darf nicht leer sein!';
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"extends": "@quasar/app/tsconfig-preset",
|
||||||
|
"target": "esnext",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"@flaschengeist/types",
|
||||||
|
"@quasar/app",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ['@quasar/babel-preset-app'],
|
||||||
'@quasar/babel-preset-app'
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit f245cb8b16c855c059d9170611797028c600696a
|
|
81
package.json
|
@ -2,42 +2,45 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "2.0.0-alpha.1",
|
"version": "2.0.0-alpha.1",
|
||||||
"productName": "Flaschengeist",
|
"productName": "flaschengeist-frontend",
|
||||||
"name": "flaschengeist-frontend",
|
"name": "flaschengeist",
|
||||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||||
"description": "Modular student club administration system",
|
"description": "Modular student club administration system",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
"format": "prettier --config ./package.json --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./src"
|
"lint": "eslint --ext .js,.ts,.vue ./src ./api"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"@flaschengeist/api": "file:./api",
|
||||||
"cordova": "^10.0.0",
|
"@flaschengeist/users": "^1.0.0-alpha.3",
|
||||||
"pinia": "^2.0.0-alpha.10",
|
"axios": "^0.24.0",
|
||||||
"quasar": "^2.0.0-beta.12",
|
"pinia": "^2.0.6",
|
||||||
"vuedraggable": "^4.0.1"
|
"quasar": "^2.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app": "^3.0.0-beta.13",
|
"@capacitor/core": "^3.3.2",
|
||||||
"@quasar/extras": "^1.10.2",
|
"@capacitor/storage": "^1.2.3",
|
||||||
"@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
|
"@flaschengeist/types": "^1.0.0-alpha.10",
|
||||||
"@types/node": "^12.20.7",
|
"@quasar/app": "^3.2.4",
|
||||||
"@types/webpack": "^4.41.27",
|
"@quasar/extras": "^1.12.2",
|
||||||
"@types/webpack-env": "^1.16.0",
|
"@types/node": "^14.18.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
"@types/webpack": "^5.28.0",
|
||||||
"@typescript-eslint/parser": "^4.20.0",
|
"@types/webpack-env": "^1.16.3",
|
||||||
"electron": "^12.0.4",
|
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||||
"electron-packager": "^14.1.1",
|
"@typescript-eslint/parser": "^5.5.0",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^8.4.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-vue": "^7.8.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-webpack-plugin": "^2.5.3",
|
"eslint-plugin-vue": "^8.1.1",
|
||||||
"prettier": "^2.2.1",
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
"typescript": "^4.2.3"
|
"modify-source-webpack-plugin": "^3.0.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"typescript": "^4.5.2",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
@ -45,19 +48,25 @@
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"arrowParens": "always"
|
"arrowParens": "always"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": {
|
||||||
"last 10 Chrome versions",
|
"defaults": [
|
||||||
"last 10 Firefox versions",
|
"Firefox esr",
|
||||||
|
"last 6 Chrome versions",
|
||||||
|
"last 4 Firefox versions",
|
||||||
"last 4 Edge versions",
|
"last 4 Edge versions",
|
||||||
"last 4 Safari versions",
|
"last 4 Safari versions",
|
||||||
"last 8 Android versions",
|
"last 4 ChromeAndroid versions",
|
||||||
"last 1 ChromeAndroid versions",
|
"last 1 FirefoxAndroid versions"
|
||||||
"last 1 FirefoxAndroid versions",
|
|
||||||
"last 6 iOS versions"
|
|
||||||
],
|
],
|
||||||
|
"cordova": [
|
||||||
|
"iOS >= 13.0",
|
||||||
|
"Android >= 76",
|
||||||
|
"ChromeAndroid >= 76"
|
||||||
|
]
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0",
|
"node": ">= 14.18.1",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.14.12",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// You can add your plugins here
|
||||||
|
module.exports = [
|
||||||
|
// '@flaschengeist/balance',
|
||||||
|
// '@flaschengeist/schedule',
|
||||||
|
// '@flaschengeist/pricelist',
|
||||||
|
]
|
|
@ -8,12 +8,12 @@
|
||||||
|
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
const { ModifySourcePlugin } = require('modify-source-webpack-plugin');
|
||||||
const { configure } = require('quasar/wrappers');
|
const { configure } = require('quasar/wrappers');
|
||||||
|
|
||||||
module.exports = configure(function (/* ctx */) {
|
module.exports = configure(function (/* ctx */) {
|
||||||
return {
|
return {
|
||||||
// https://quasar.dev/quasar-cli/supporting-ts
|
|
||||||
// https://quasar.dev/quasar-cli/supporting-ts
|
// https://quasar.dev/quasar-cli/supporting-ts
|
||||||
supportTS: {
|
supportTS: {
|
||||||
tsCheckerConfig: {
|
tsCheckerConfig: {
|
||||||
|
@ -21,7 +21,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/prefetch-feature
|
// https://quasar.dev/quasar-cli/prefetch-feature
|
||||||
|
@ -30,7 +30,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://quasar.dev/quasar-cli/boot-files
|
// https://quasar.dev/quasar-cli/boot-files
|
||||||
boot: ['axios', 'store', 'plugins', 'loading', 'login'],
|
boot: ['axios', 'store', 'plugins', 'login', 'init'],
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
||||||
css: ['app.scss'],
|
css: ['app.scss'],
|
||||||
|
@ -39,10 +39,10 @@ module.exports = configure(function (/* ctx */) {
|
||||||
extras: [
|
extras: [
|
||||||
// 'eva-icons',
|
// 'eva-icons',
|
||||||
// 'fontawesome-v5',
|
// 'fontawesome-v5',
|
||||||
// 'ionicons-v4',
|
// 'ionicons-v5',
|
||||||
// 'line-awesome',
|
// 'line-awesome',
|
||||||
// 'material-icons',
|
// 'material-icons',
|
||||||
'mdi-v5',
|
'mdi-v6',
|
||||||
// 'themify',
|
// 'themify',
|
||||||
|
|
||||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
@ -60,10 +60,8 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// Applies only if "transpile" is set to true.
|
// Applies only if "transpile" is set to true.
|
||||||
// transpileDependencies: [],
|
// transpileDependencies: [],
|
||||||
|
|
||||||
// rtl: false, // https://quasar.dev/options/rtl-support
|
// rtl: false,
|
||||||
// preloadChunks: true,
|
|
||||||
// showProgress: false,
|
|
||||||
// gzip: true,
|
|
||||||
// analyze: true,
|
// analyze: true,
|
||||||
|
|
||||||
// Options below are automatically set depending on the env, set them if you want to override
|
// Options below are automatically set depending on the env, set them if you want to override
|
||||||
|
@ -72,33 +70,58 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// https://quasar.dev/quasar-cli/handling-webpack
|
// https://quasar.dev/quasar-cli/handling-webpack
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
chainWebpack(chain) {
|
chainWebpack(chain) {
|
||||||
chain.plugin('eslint-webpack-plugin')
|
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
|
||||||
.use(ESLintPlugin, [{
|
{
|
||||||
extensions: ['ts', 'js', 'vue'],
|
extensions: ['ts', 'js', 'vue'],
|
||||||
exclude: 'node_modules'
|
exclude: ['node_modules', 'src-capacitor'],
|
||||||
}])
|
},
|
||||||
|
]);
|
||||||
|
chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
|
||||||
|
{
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /plugins\.ts$/,
|
||||||
|
modify: (src, filename) => {
|
||||||
|
const custom_plgns = require('./plugin.config.js');
|
||||||
|
const required_plgns = require('./src/vendor-plugin.config.js');
|
||||||
|
return src.replace(
|
||||||
|
/\/\* *INSERT_PLUGIN_LIST *\*\//,
|
||||||
|
[...custom_plgns, ...required_plgns]
|
||||||
|
.map((v) => `import("${v}").catch(() => "${v}")`)
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
chain.merge({
|
||||||
|
snapshot: {
|
||||||
|
managedPaths: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
||||||
devServer: {
|
devServer: {
|
||||||
https: false,
|
https: false,
|
||||||
port: 8080,
|
port: 8080,
|
||||||
open: false // opens browser window automatically
|
open: false, // opens browser window automatically
|
||||||
|
watchFiles: { paths: ['/node_modules/@flaschengeist/**/*'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
||||||
framework: {
|
framework: {
|
||||||
iconSet: 'mdi-v5', // Quasar icon set
|
iconSet: 'mdi-v6', // Quasar icon set
|
||||||
lang: 'de', // Quasar language pack
|
lang: 'de', // Quasar language pack
|
||||||
config: {
|
config: {
|
||||||
dark: 'auto',
|
dark: 'auto',
|
||||||
loadingBar: {
|
loadingBar: {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
size: '5px'
|
size: '5px',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// For special cases outside of where the auto-import stategy can have an impact
|
// For special cases outside of where the auto-import stategy can have an impact
|
||||||
|
@ -109,13 +132,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// directives: [],
|
// directives: [],
|
||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: [
|
plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
|
||||||
'LocalStorage',
|
|
||||||
'SessionStorage',
|
|
||||||
'Loading',
|
|
||||||
'Notify',
|
|
||||||
'LoadingBar'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
|
@ -124,7 +141,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||||
ssr: {
|
ssr: {
|
||||||
pwa: false
|
pwa: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
||||||
|
@ -143,20 +160,20 @@ module.exports = configure(function (/* ctx */) {
|
||||||
{
|
{
|
||||||
src: 'flaschengeist-logo.svg',
|
src: 'flaschengeist-logo.svg',
|
||||||
sizes: 'any',
|
sizes: 'any',
|
||||||
type: 'image/svg+xml'
|
type: 'image/svg+xml',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'favicon-128x128.png',
|
src: 'favicon-128x128.png',
|
||||||
sizes: '128x128',
|
sizes: '128x128',
|
||||||
type: 'image/png'
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'favicon-256x256.png',
|
src: 'favicon-256x256.png',
|
||||||
sizes: '256x256',
|
sizes: '256x256',
|
||||||
type: 'image/png'
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
||||||
|
@ -166,7 +183,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
||||||
capacitor: {
|
capacitor: {
|
||||||
hideSplashscreen: true
|
hideSplashscreen: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
||||||
|
@ -175,13 +192,11 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
packager: {
|
packager: {
|
||||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||||
|
|
||||||
// OS X / Mac App Store
|
// OS X / Mac App Store
|
||||||
// appBundleId: '',
|
// appBundleId: '',
|
||||||
// appCategoryType: '',
|
// appCategoryType: '',
|
||||||
// osxSign: '',
|
// osxSign: '',
|
||||||
// protocol: 'myapp://path',
|
// protocol: 'myapp://path',
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
// win32metadata: { ... }
|
// win32metadata: { ... }
|
||||||
},
|
},
|
||||||
|
@ -189,7 +204,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
builder: {
|
builder: {
|
||||||
// https://www.electron.build/configuration/configuration
|
// https://www.electron.build/configuration/configuration
|
||||||
|
|
||||||
appId: 'flaschengeist-frontend'
|
appId: 'flaschengeist-frontend',
|
||||||
},
|
},
|
||||||
|
|
||||||
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
|
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
|
||||||
|
@ -198,7 +213,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
extendWebpack(/* cfg */) {
|
extendWebpack(/* cfg */) {
|
||||||
// do something with Electron main process Webpack cfg
|
// do something with Electron main process Webpack cfg
|
||||||
// chainWebpack also available besides this extendWebpack
|
// chainWebpack also available besides this extendWebpack
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"@quasar/qcalendar": {}
|
|
||||||
}
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
/* eslint-disable */
|
||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
import 'quasar/dist/types/feature-flag';
|
import 'quasar/dist/types/feature-flag';
|
||||||
|
|
||||||
declare module 'quasar/dist/types/feature-flag' {
|
declare module 'quasar/dist/types/feature-flag' {
|
||||||
interface QuasarFeatureFlags {
|
interface QuasarFeatureFlags {
|
||||||
store: true;
|
capacitor: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"appId": "dev.flaschengeist",
|
||||||
|
"appName": "flaschengeist-frontend",
|
||||||
|
"bundledWebRuntime": false,
|
||||||
|
"npmClient": "yarn",
|
||||||
|
"webDir": "www",
|
||||||
|
"ios": {
|
||||||
|
"allowsLinkPreview": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "flaschengeist",
|
||||||
|
"version": "2.0.0-alpha.1",
|
||||||
|
"description": "Modular student club administration system",
|
||||||
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor/android": "^3.3.2",
|
||||||
|
"@capacitor/app": "^1.0.0",
|
||||||
|
"@capacitor/cli": "^3.0.0",
|
||||||
|
"@capacitor/core": "^3.0.0",
|
||||||
|
"@capacitor/ios": "^3.0.0-beta.0",
|
||||||
|
"@capacitor/splash-screen": "^1.0.0",
|
||||||
|
"@capacitor/storage": "^1.2.3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Generated by package manager
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Generated by Cordova
|
|
||||||
/plugins/
|
|
||||||
/platforms/
|
|
|
@ -1,76 +0,0 @@
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<widget id="de.wu5.flaschengeist" version="2.0.0-alpha.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
|
||||||
<name>Flaschengeist</name>
|
|
||||||
<description>Modular student club administration system</description>
|
|
||||||
<author email="dev@cordova.apache.org" href="http://cordova.io">
|
|
||||||
Apache Cordova Team
|
|
||||||
</author>
|
|
||||||
<content src="index.html" />
|
|
||||||
<access origin="*" />
|
|
||||||
<allow-intent href="http://*/*" />
|
|
||||||
<allow-intent href="https://*/*" />
|
|
||||||
<allow-intent href="tel:*" />
|
|
||||||
<allow-intent href="sms:*" />
|
|
||||||
<allow-intent href="mailto:*" />
|
|
||||||
<allow-intent href="geo:*" />
|
|
||||||
<platform name="android">
|
|
||||||
<allow-intent href="market:*" />
|
|
||||||
<icon density="ldpi" src="res/android/ldpi.png" />
|
|
||||||
<icon density="mdpi" src="res/android/mdpi.png" />
|
|
||||||
<icon density="hdpi" src="res/android/hdpi.png" />
|
|
||||||
<icon density="xhdpi" src="res/android/xhdpi.png" />
|
|
||||||
<icon density="xxhdpi" src="res/android/xxhdpi.png" />
|
|
||||||
<icon density="xxxhdpi" src="res/android/xxxhdpi.png" />
|
|
||||||
<splash density="land-ldpi" src="res/screen/android/splash-land-ldpi.png" />
|
|
||||||
<splash density="port-ldpi" src="res/screen/android/splash-port-ldpi.png" />
|
|
||||||
<splash density="land-mdpi" src="res/screen/android/splash-land-mdpi.png" />
|
|
||||||
<splash density="port-mdpi" src="res/screen/android/splash-port-mdpi.png" />
|
|
||||||
<splash density="land-hdpi" src="res/screen/android/splash-land-hdpi.png" />
|
|
||||||
<splash density="port-hdpi" src="res/screen/android/splash-port-hdpi.png" />
|
|
||||||
<splash density="land-xhdpi" src="res/screen/android/splash-land-xhdpi.png" />
|
|
||||||
<splash density="port-xhdpi" src="res/screen/android/splash-port-xhdpi.png" />
|
|
||||||
<splash density="land-xxhdpi" src="res/screen/android/splash-land-xxhdpi.png" />
|
|
||||||
<splash density="port-xxhdpi" src="res/screen/android/splash-port-xxhdpi.png" />
|
|
||||||
<splash density="land-xxxhdpi" src="res/screen/android/splash-land-xxxhdpi.png" />
|
|
||||||
<splash density="port-xxxhdpi" src="res/screen/android/splash-port-xxxhdpi.png" />
|
|
||||||
</platform>
|
|
||||||
<platform name="ios">
|
|
||||||
<allow-intent href="itms:*" />
|
|
||||||
<allow-intent href="itms-apps:*" />
|
|
||||||
<icon height="57" src="res/ios/icon.png" width="57" />
|
|
||||||
<icon height="114" src="res/ios/icon@2x.png" width="114" />
|
|
||||||
<icon height="40" src="res/ios/icon-20@2x.png" width="40" />
|
|
||||||
<icon height="60" src="res/ios/icon-20@3x.png" width="60" />
|
|
||||||
<icon height="29" src="res/ios/icon-29.png" width="29" />
|
|
||||||
<icon height="58" src="res/ios/icon-29@2x.png" width="58" />
|
|
||||||
<icon height="87" src="res/ios/icon-29@3x.png" width="87" />
|
|
||||||
<icon height="80" src="res/ios/icon-40@2x.png" width="80" />
|
|
||||||
<icon height="120" src="res/ios/icon-60@2x.png" width="120" />
|
|
||||||
<icon height="180" src="res/ios/icon-60@3x.png" width="180" />
|
|
||||||
<icon height="20" src="res/ios/icon-20.png" width="20" />
|
|
||||||
<icon height="40" src="res/ios/icon-40.png" width="40" />
|
|
||||||
<icon height="50" src="res/ios/icon-50.png" width="50" />
|
|
||||||
<icon height="100" src="res/ios/icon-50@2x.png" width="100" />
|
|
||||||
<icon height="72" src="res/ios/icon-72.png" width="72" />
|
|
||||||
<icon height="144" src="res/ios/icon-72@2x.png" width="144" />
|
|
||||||
<icon height="76" src="res/ios/icon-76.png" width="76" />
|
|
||||||
<icon height="152" src="res/ios/icon-76@2x.png" width="152" />
|
|
||||||
<icon height="167" src="res/ios/icon-83.5@2x.png" width="167" />
|
|
||||||
<icon height="1024" src="res/ios/icon-1024.png" width="1024" />
|
|
||||||
<icon height="48" src="res/ios/icon-24@2x.png" width="48" />
|
|
||||||
<icon height="55" src="res/ios/icon-27.5@2x.png" width="55" />
|
|
||||||
<icon height="88" src="res/ios/icon-44@2x.png" width="88" />
|
|
||||||
<icon height="172" src="res/ios/icon-86@2x.png" width="172" />
|
|
||||||
<icon height="196" src="res/ios/icon-98@2x.png" width="196" />
|
|
||||||
<splash src="res/screen/ios/Default@2x~iphone~anyany.png" />
|
|
||||||
<splash src="res/screen/ios/Default@2x~iphone~comany.png" />
|
|
||||||
<splash src="res/screen/ios/Default@2x~iphone~comcom.png" />
|
|
||||||
<splash src="res/screen/ios/Default@3x~iphone~anyany.png" />
|
|
||||||
<splash src="res/screen/ios/Default@3x~iphone~anycom.png" />
|
|
||||||
<splash src="res/screen/ios/Default@3x~iphone~comany.png" />
|
|
||||||
<splash src="res/screen/ios/Default@2x~ipad~anyany.png" />
|
|
||||||
<splash src="res/screen/ios/Default@2x~ipad~comany.png" />
|
|
||||||
</platform>
|
|
||||||
<allow-navigation href="about:*" />
|
|
||||||
<preference name="SplashMaintainAspectRatio" value="true" />
|
|
||||||
</widget>
|
|
|
@ -1,10 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
|
||||||
import "quasar/dist/types/feature-flag";
|
|
||||||
|
|
||||||
declare module "quasar/dist/types/feature-flag" {
|
|
||||||
interface QuasarFeatureFlags {
|
|
||||||
cordova: true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"name": "de.wu5.flaschengeist",
|
|
||||||
"displayName": "Flaschengeist",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A sample Apache Cordova application that responds to the deviceready event.",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"ecosystem:cordova"
|
|
||||||
],
|
|
||||||
"author": "Apache Cordova Team",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"cordova-android": "^9.0.0",
|
|
||||||
"cordova-ios": "^6.1.1",
|
|
||||||
"cordova-plugin-splashscreen": "^6.0.0",
|
|
||||||
"cordova-plugin-whitelist": "^1.3.4"
|
|
||||||
},
|
|
||||||
"cordova": {
|
|
||||||
"plugins": {
|
|
||||||
"cordova-plugin-whitelist": {},
|
|
||||||
"cordova-plugin-splashscreen": {}
|
|
||||||
},
|
|
||||||
"platforms": [
|
|
||||||
"ios",
|
|
||||||
"android"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 702 B |
Before Width: | Height: | Size: 1022 B |
Before Width: | Height: | Size: 801 B |
Before Width: | Height: | Size: 969 B |
Before Width: | Height: | Size: 529 B |
Before Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 702 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 885 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 996 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 8.5 KiB |
|
@ -1,9 +1,9 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
import "quasar/dist/types/feature-flag";
|
import 'quasar/dist/types/feature-flag';
|
||||||
|
|
||||||
declare module "quasar/dist/types/feature-flag" {
|
declare module 'quasar/dist/types/feature-flag' {
|
||||||
interface QuasarFeatureFlags {
|
interface QuasarFeatureFlags {
|
||||||
electron: true;
|
electron: true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,77 @@
|
||||||
import config from 'src/config';
|
/**
|
||||||
|
* This boot file registers interceptors for axios
|
||||||
|
*/
|
||||||
|
import { useMainStore, api } from '@flaschengeist/api';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { LocalStorage, Notify } from 'quasar';
|
import config from 'src/config';
|
||||||
import axios, { AxiosError } from 'axios';
|
import { clone } from '@flaschengeist/api';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
const api = axios.create();
|
/**
|
||||||
|
* Minify data sent to backend server
|
||||||
|
*
|
||||||
|
* Drop unneeded entities which can be identified by ID.
|
||||||
|
*
|
||||||
|
* @param obj Object to minify
|
||||||
|
* @param cloned If this entity is already cloned (JSON En+Decoded)
|
||||||
|
* @returns Minified object (some types are converted, like a Date object is now a ISO string)
|
||||||
|
*/
|
||||||
|
function minify(entity: unknown, cloned = false) {
|
||||||
|
if (!cloned) entity = clone(entity);
|
||||||
|
|
||||||
|
if (typeof entity === 'object') {
|
||||||
|
const obj = entity as { [index: string]: unknown };
|
||||||
|
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (obj.hasOwnProperty(prop) && !!obj[prop]) {
|
||||||
|
if (Array.isArray(obj[prop])) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
|
||||||
|
} else if (
|
||||||
|
typeof obj[prop] === 'object' &&
|
||||||
|
Object.keys(<object>obj[prop]).includes('id') &&
|
||||||
|
typeof (<{ id: unknown }>obj[prop])['id'] === 'number' &&
|
||||||
|
!isNaN((<{ id: number }>obj[prop])['id'])
|
||||||
|
) {
|
||||||
|
obj[prop] = (<{ id: unknown }>obj[prop])['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
|
// Persisted value is read in plugins.ts boot file!
|
||||||
|
if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Intercept requests and insert Token if available
|
* Intercept requests
|
||||||
|
* - insert Token if available
|
||||||
|
* - minify JSON requests
|
||||||
*/
|
*/
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
if (store.session?.token) {
|
if (store.session?.token) {
|
||||||
config.headers = { Authorization: 'Bearer ' + store.session.token };
|
config.headers = Object.assign(config.headers || {}, {
|
||||||
|
Authorization: `Bearer ${store.session.token}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
// Minify JSON requests
|
||||||
|
if (
|
||||||
|
!!config.data &&
|
||||||
|
(config.headers === undefined ||
|
||||||
|
config.headers['Content-Type'] === undefined ||
|
||||||
|
config.headers['Content-Type'] === 'application/json')
|
||||||
|
)
|
||||||
|
config.data = minify(config.data);
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Intercept responses
|
* Intercept responses
|
||||||
* - filter 401 --> logout
|
* - filter 401 --> handleLoggedOut
|
||||||
* - filter timeout or 502-504 --> backendOffline
|
* - filter timeout or 502-504 --> backendOffline
|
||||||
*/
|
*/
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
|
@ -45,12 +94,11 @@ export default boot(({ router }) => {
|
||||||
query: { redirect: next },
|
query: { redirect: next },
|
||||||
});
|
});
|
||||||
} else if (e.response && e.response.status == 401) {
|
} else if (e.response && e.response.status == 401) {
|
||||||
void store.logout();
|
store.handleLoggedOut();
|
||||||
if (current.name !== 'login') {
|
if (current.name != 'login') {
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
params: { logout: 'logout' },
|
query: { redirect: current.fullPath },
|
||||||
query: { redirect: current.path },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,19 +107,3 @@ export default boot(({ router }) => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export { api };
|
|
||||||
|
|
||||||
export const setBaseURL = (url: string) => {
|
|
||||||
LocalStorage.set('baseURL', url);
|
|
||||||
api.defaults.baseURL = url;
|
|
||||||
Notify.create({
|
|
||||||
message: 'Serveraddresse gespeichert',
|
|
||||||
position: 'bottom',
|
|
||||||
caption: `${url}`,
|
|
||||||
color: 'positive',
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* This boot file initalizes the store from persistent storage and load all plugins
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
PersistentStorage,
|
||||||
|
api,
|
||||||
|
isAxiosError,
|
||||||
|
saveSession,
|
||||||
|
useMainStore,
|
||||||
|
} from '@flaschengeist/api';
|
||||||
|
import { Notify, Platform } from 'quasar';
|
||||||
|
import { loadPlugins } from './plugins';
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import routes from 'src/router/routes';
|
||||||
|
|
||||||
|
async function loadBaseUrl() {
|
||||||
|
try {
|
||||||
|
const url = await PersistentStorage.get<string>('baseURL');
|
||||||
|
if (url !== null) api.defaults.baseURL = url;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not load BaseURL', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackendError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading backend information
|
||||||
|
* @returns Backend object or null
|
||||||
|
*/
|
||||||
|
async function getBackend() {
|
||||||
|
const { data } = await api.get<FG.Backend>('/');
|
||||||
|
if (!data || typeof data !== 'object' || !('plugins' in data))
|
||||||
|
throw new BackendError('Invalid backend response received');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot file for loading baseURL + Session from PersistentStorage + loading and initializing all plugins
|
||||||
|
*/
|
||||||
|
export default boot(async ({ app, router }) => {
|
||||||
|
const store = useMainStore();
|
||||||
|
|
||||||
|
// FIRST(!) get the base URL
|
||||||
|
await loadBaseUrl();
|
||||||
|
|
||||||
|
// Init the store, load current session and user, if available
|
||||||
|
try {
|
||||||
|
await store.init();
|
||||||
|
} finally {
|
||||||
|
// Any changes on the session is written back to the persistent store
|
||||||
|
store.$subscribe((mutation, state) => {
|
||||||
|
saveSession(state.session);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all plugins
|
||||||
|
try {
|
||||||
|
// Fetch backend data
|
||||||
|
const backend = await getBackend();
|
||||||
|
// Load enabled plugins
|
||||||
|
const flaschengeist = await loadPlugins(backend, routes);
|
||||||
|
// Add loaded routes to router
|
||||||
|
flaschengeist.routes.forEach((route) => router.addRoute(route));
|
||||||
|
// save plugins in VM-variable
|
||||||
|
app.provide('flaschengeist', flaschengeist);
|
||||||
|
} catch (error) {
|
||||||
|
// Handle errors from loading the backend information
|
||||||
|
if (error instanceof BackendError || isAxiosError(error)) {
|
||||||
|
router.isReady().finally(() => {
|
||||||
|
if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
|
||||||
|
else void router.push({ name: 'offline', params: { refresh: 1 } });
|
||||||
|
});
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
// Handle plugin not found errors
|
||||||
|
void router.push({ name: 'error' });
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Fehler beim Laden: Bitte wende dich an den Admin (${error})!`,
|
||||||
|
timeout: 10000,
|
||||||
|
progress: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Unknown error in init.ts:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,14 +0,0 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import { Loading } from 'quasar';
|
|
||||||
//import DarkCircularProgress from 'components/loading/DarkCircularProgress.vue';
|
|
||||||
|
|
||||||
// "async" is optional;
|
|
||||||
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
|
|
||||||
export default boot(() => {
|
|
||||||
Loading.setDefaults({
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
// spinner: DarkCircularProgress,
|
|
||||||
// TODO : Das funktioniert wohl erstmal nicht mehr... gibt ne exception
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,43 +1,33 @@
|
||||||
|
/**
|
||||||
|
* This boot file registers login / authentification related axios interceptors
|
||||||
|
*/
|
||||||
|
import { useMainStore, hasPermissions } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
|
||||||
import { RouteRecord } from 'vue-router';
|
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
router.beforeResolve((to, from, next) => {
|
/**
|
||||||
|
* Login guard
|
||||||
|
* Check if user tries to access the secured area and validates token
|
||||||
|
*/
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
|
|
||||||
if (to.path == from.path) return next();
|
// Skip loops
|
||||||
|
if (to.name == 'login' && from.name == 'login') return false;
|
||||||
|
|
||||||
if (to.path.startsWith('/main')) {
|
// Secured area '/in/...' requires to be authenticated
|
||||||
// Secured area (LOGIN REQUIRED)
|
if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
|
||||||
// Check login is ok
|
store.handleLoggedOut();
|
||||||
if (!store.session || store.session.expires <= new Date()) {
|
return { name: 'login' };
|
||||||
void store.logout();
|
|
||||||
return next({ name: 'login', query: { redirect: to.fullPath } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if special permissions are required
|
|
||||||
if (
|
|
||||||
to.matched.every((record: RouteRecord) => {
|
|
||||||
if (!('meta' in record) || !('permissions' in record.meta)) return true;
|
|
||||||
if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
|
|
||||||
return hasPermissions((<{ permissions: FG.Permission[] }>record.meta).permissions);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return next();
|
|
||||||
} else {
|
|
||||||
return next({ name: 'login', query: { redirect: to.fullPath } });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (to.name == 'login' && store.user && !to.params['logout']) {
|
|
||||||
// Called login while already logged in
|
|
||||||
return next({ name: 'dashboard' });
|
|
||||||
} else {
|
|
||||||
// We are on the non secured area
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission guard
|
||||||
|
* Check permissions for route, cancel navigation on errors
|
||||||
|
*/
|
||||||
|
router.beforeResolve((to) => {
|
||||||
|
if (!!to.meta.permissions && !hasPermissions(<FG.Permission[]>to.meta.permissions))
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,60 +1,25 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
|
||||||
import routes from 'src/router/routes';
|
|
||||||
import { api } from 'boot/axios';
|
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import { Notify } from 'quasar';
|
|
||||||
|
|
||||||
const config: { [key: string]: Array<string> } = {
|
|
||||||
// Do not change required Modules !!
|
|
||||||
requiredModules: ['User'],
|
|
||||||
// here you can import plugins.
|
|
||||||
loadModules: ['Balance', 'Schedule', 'Pricelist'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Stop!
|
|
||||||
|
|
||||||
// do not change anything here !!
|
|
||||||
|
|
||||||
// You can not even read? I said stop!
|
|
||||||
|
|
||||||
// Really are you stupid? Stop scrolling down here!
|
|
||||||
|
|
||||||
// Every line you scroll down, an unicorn will die painfully!
|
|
||||||
|
|
||||||
// Ok you must hate unicorns... But what if I say you I joked... Baby otters will die!
|
|
||||||
|
|
||||||
.-"""-.
|
|
||||||
/ o\
|
|
||||||
| o 0).-.
|
|
||||||
| .-;(_/ .-.
|
|
||||||
\ / /)).---._| `\ ,
|
|
||||||
'. ' /(( `'-./ _/|
|
|
||||||
\ .' ) .-.;` /
|
|
||||||
'. | `\-'
|
|
||||||
'._ -' /
|
|
||||||
``""--`------`
|
|
||||||
*/
|
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
******** Internal area for some magic **************
|
******** Internal area for some magic **************
|
||||||
****************************************************/
|
****************************************************/
|
||||||
|
|
||||||
interface BackendPlugin {
|
declare type ImportPlgn = { default: FG_Plugin.Plugin };
|
||||||
permissions: string[];
|
|
||||||
version: string;
|
function validatePlugin(plugin: FG_Plugin.Plugin) {
|
||||||
|
return (
|
||||||
|
typeof plugin.name === 'string' &&
|
||||||
|
typeof plugin.id === 'string' &&
|
||||||
|
plugin.id.length > 0 &&
|
||||||
|
typeof plugin.version === 'string'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackendPlugins {
|
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
|
||||||
[key: string]: BackendPlugin;
|
const PLUGINS = <Array<Promise<ImportPlgn>>>[
|
||||||
}
|
/*INSERT_PLUGIN_LIST*/
|
||||||
|
];
|
||||||
interface Backend {
|
|
||||||
plugins: BackendPlugins;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
export { Backend };
|
|
||||||
|
|
||||||
// Handle Notifications
|
// Handle Notifications
|
||||||
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
||||||
|
@ -221,46 +186,27 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
|
||||||
/**
|
/**
|
||||||
* Load a Flaschengeist plugin
|
* Load a Flaschengeist plugin
|
||||||
* @param loadedPlugins Flaschgeist object
|
* @param loadedPlugins Flaschgeist object
|
||||||
* @param pluginName Plugin to load
|
* @param plugin Plugin to load
|
||||||
* @param context RequireContext of plugins
|
|
||||||
* @param router VueRouter instance
|
* @param router VueRouter instance
|
||||||
*/
|
*/
|
||||||
function loadPlugin(
|
function loadPlugin(
|
||||||
loadedPlugins: FG_Plugin.Flaschengeist,
|
loadedPlugins: FG_Plugin.Flaschengeist,
|
||||||
pluginName: string,
|
plugin: FG_Plugin.Plugin,
|
||||||
context: __WebpackModuleApi.RequireContext,
|
backend: FG.Backend
|
||||||
backend: Backend
|
|
||||||
) {
|
) {
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true;
|
if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
|
||||||
|
|
||||||
// Search if plugin is installed
|
// Check backend dependencies
|
||||||
const available = context.keys();
|
|
||||||
const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
|
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
<FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!plugin) {
|
|
||||||
// Plugin is not found, results in an error
|
|
||||||
console.exception(`Could not find required Plugin ${pluginName}`);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// Plugin found. Check backend dependencies
|
|
||||||
if (
|
if (
|
||||||
!plugin.requiredBackendModules.every((required) => backend.plugins[required] !== undefined)
|
!plugin.requiredModules.every(
|
||||||
) {
|
(required) =>
|
||||||
console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
|
backend.plugins[required[0]] !== undefined &&
|
||||||
return false;
|
(required.length == 1 ||
|
||||||
}
|
true) /* validate the version, semver440 from python is... tricky on node*/
|
||||||
|
|
||||||
// Check frontend dependencies
|
|
||||||
if (
|
|
||||||
!plugin.requiredModules.every((required) =>
|
|
||||||
loadPlugin(loadedPlugins, required, context, backend)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
|
console.error(`Plugin ${plugin.id}: Backend modules not satisfied`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,46 +230,23 @@ function loadPlugin(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.widgets.length > 0) {
|
if (plugin.widgets.length > 0) {
|
||||||
plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
|
plugin.widgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
|
||||||
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedPlugins.plugins.push({
|
loadedPlugins.plugins.push({
|
||||||
|
id: plugin.id,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
version: plugin.version,
|
version: plugin.version,
|
||||||
notification: plugin.notification?.bind({}) || translateNotification,
|
notification: plugin.notification?.bind({}) || translateNotification,
|
||||||
});
|
});
|
||||||
|
|
||||||
return plugin;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading backend information
|
|
||||||
* @returns Backend object or null
|
|
||||||
*/
|
|
||||||
async function getBackend() {
|
|
||||||
try {
|
|
||||||
const { data }: AxiosResponse<Backend> = await api.get('/');
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot file, load all required plugins, check for dependencies
|
|
||||||
*/
|
|
||||||
export default boot(async ({ router, app }) => {
|
|
||||||
const backend = await getBackend();
|
|
||||||
if (backend === null) {
|
|
||||||
void router.push({ name: 'error' });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
|
||||||
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
||||||
routes,
|
routes: baseRoutes,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
menuLinks: [],
|
menuLinks: [],
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
|
@ -331,47 +254,35 @@ export default boot(async ({ router, app }) => {
|
||||||
widgets: [],
|
widgets: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// get all plugins
|
// Wait for all plugins to be loaded
|
||||||
const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
|
const results = await Promise.allSettled(PLUGINS);
|
||||||
|
|
||||||
// Start loading plugins
|
// Check if loaded successfully
|
||||||
// Load required modules, if not found or error when loading this will forward the user to the error page
|
results.forEach((result) => {
|
||||||
config.requiredModules.forEach((required) => {
|
if (result.status === 'rejected') {
|
||||||
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
throw <string>result.reason;
|
||||||
if (!plugin) {
|
} else {
|
||||||
void router.push({ name: 'error' });
|
if (
|
||||||
return;
|
!(
|
||||||
|
validatePlugin(result.value.default) &&
|
||||||
|
loadPlugin(loadedPlugins, result.value.default, backend)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw result.value.default.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load user defined plugins
|
|
||||||
// If there is an error with loading a plugin, the user will get informed.
|
|
||||||
const failed: string[] = [];
|
|
||||||
config.loadModules.forEach((required) => {
|
|
||||||
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
|
||||||
if (!plugin) {
|
|
||||||
failed.push(required);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (failed.length > 0) {
|
|
||||||
// Log failed plugins
|
|
||||||
console.error('Could not load all plugins', failed);
|
|
||||||
// Inform user about error
|
|
||||||
Notify.create({
|
|
||||||
type: 'negative',
|
|
||||||
message:
|
|
||||||
'Fehler beim Laden: Nicht alle Funktionen stehen zur Verfügung. Bitte wende dich an den Admin!',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort widgets by priority
|
// Sort widgets by priority
|
||||||
loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
|
/** @todo Remove priority with first beta */
|
||||||
|
loadedPlugins.widgets.sort(
|
||||||
|
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
|
||||||
|
);
|
||||||
|
|
||||||
// Add loaded routes to router
|
/** @todo Can be cleaned up with first beta */
|
||||||
loadedPlugins.routes.forEach((route) => router.addRoute(route));
|
loadedPlugins.menuLinks.sort((a, b) => {
|
||||||
|
const diff = a.order && b.order ? b.order - a.order : 0;
|
||||||
// save plugins in VM-variable
|
return diff ? diff : a.title.toString().localeCompare(b.title.toString());
|
||||||
app.provide('flaschengeist', loadedPlugins);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return loadedPlugins;
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { createPinia, Pinia } from 'pinia';
|
/**
|
||||||
|
* This boot file installs the global pinia instance
|
||||||
|
*/
|
||||||
|
import { pinia } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
export const pinia = ref<Pinia>();
|
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
pinia.value = createPinia();
|
app.use(pinia);
|
||||||
app.use(pinia.value);
|
|
||||||
|
|
||||||
const store = useMainStore();
|
|
||||||
void store.init();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,41 +1,59 @@
|
||||||
<template>
|
<template>
|
||||||
<q-card
|
<q-card
|
||||||
bordered
|
bordered
|
||||||
class="row q-ma-xs q-pa-xs"
|
|
||||||
style="position: relative; min-height: 3em"
|
style="position: relative; min-height: 3em"
|
||||||
:class="{ 'cursor-pointer': modelValue.link }"
|
:class="{ 'cursor-pointer': modelValue.link }"
|
||||||
@click="click"
|
@click="click"
|
||||||
>
|
>
|
||||||
<div class="col-12 text-weight-light">{{ dateString }}</div>
|
|
||||||
<div :id="`ntfctn-${modelValue.id}`" class="col-12">{{ modelValue.text }}</div>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
dense
|
dense
|
||||||
icon="mdi-close"
|
icon="mdi-trash-can"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="negative"
|
color="negative"
|
||||||
class="q-ma-xs"
|
class="q-ma-xs"
|
||||||
style="position: absolute; top: 0; right: 0"
|
title="Löschen"
|
||||||
@click="remove"
|
style="position: absolute; top: 0; right: 0; z-index: 999"
|
||||||
|
@click="dismiss"
|
||||||
/>
|
/>
|
||||||
|
<q-card-section class="q-pa-xs">
|
||||||
|
<div class="text-overline">{{ dateString }}</div>
|
||||||
|
<q-item style="padding: 1px">
|
||||||
|
<q-item-section v-if="modelValue.icon" side
|
||||||
|
><q-icon color="primary" :name="modelValue.icon"
|
||||||
|
/></q-item-section>
|
||||||
|
<q-item-section>{{ modelValue.text }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions v-if="modelValue.reject || modelValue.accept">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="modelValue.accept !== undefined"
|
v-if="modelValue.accept"
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="mdi-check"
|
icon="mdi-check"
|
||||||
size="sm"
|
|
||||||
color="positive"
|
color="positive"
|
||||||
class="q-ma-xs"
|
label="Annehmen"
|
||||||
style="position: absolute; top: 0; right: 3em"
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
@click="accept"
|
@click="accept"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="modelValue.reject"
|
||||||
|
icon="mdi-close"
|
||||||
|
color="negative"
|
||||||
|
label="Ablehnen"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
@click="reject"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, computed } from 'vue';
|
import { defineComponent, PropType, computed } from 'vue';
|
||||||
import { formatDateTime } from 'src/utils/datetime';
|
import { formatDateTime } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -63,13 +81,19 @@ export default defineComponent({
|
||||||
else emit('remove', props.modelValue.id);
|
else emit('remove', props.modelValue.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove() {
|
function reject() {
|
||||||
if (typeof props.modelValue.reject === 'function')
|
if (typeof props.modelValue.reject === 'function')
|
||||||
void props.modelValue.reject().finally(() => emit('remove', props.modelValue.id));
|
void props.modelValue.reject().finally(() => emit('remove', props.modelValue.id));
|
||||||
else emit('remove', props.modelValue.id);
|
else emit('remove', props.modelValue.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { accept, click, dateString, remove };
|
function dismiss() {
|
||||||
|
if (typeof props.modelValue.dismiss === 'function')
|
||||||
|
void props.modelValue.dismiss().finally(() => emit('remove', props.modelValue.id));
|
||||||
|
else emit('remove', props.modelValue.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accept, click, dateString, dismiss, reject };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<q-expansion-item v-if="isGranted(entry)" clickable tag="a" target="self" :label='title' :icon='entry.icon' expand-separator>
|
<q-expansion-item
|
||||||
<q-list class='q-ml-lg'>
|
v-if="isGranted(entry)"
|
||||||
<div v-for='child in entry.children' :key='child.link'>
|
clickable
|
||||||
<q-item v-if='isGranted(child)' clickable :to='{name: child.link}'>
|
:label="getTitle(entry)"
|
||||||
|
:icon="entry.icon"
|
||||||
|
expand-separator
|
||||||
|
>
|
||||||
|
<q-list class="q-ml-lg">
|
||||||
|
<div v-for="child in entry.children" :key="child.link">
|
||||||
|
<q-item v-if="isGranted(child)" clickable :to="{ name: child.link }">
|
||||||
<q-menu context-menu>
|
<q-menu context-menu>
|
||||||
<q-btn v-close-popup label='Verknüpfung erstellen' dense @click='addShortCut(child)'/>
|
<q-btn v-close-popup label="Verknüpfung erstellen" dense @click="addShortCut(child)" />
|
||||||
</q-menu>
|
</q-menu>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon :name='child.icon' />
|
<q-icon :name="child.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>
|
||||||
{{child.title}}
|
{{ getTitle(child) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
@ -21,9 +27,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EssentialExpansionLink',
|
name: 'EssentialExpansionLink',
|
||||||
|
@ -37,17 +43,19 @@ export default defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
addShortCut: (val: FG_Plugin.MenuLink) => val.link,
|
addShortCut: (val: FG_Plugin.MenuLink) => val.link,
|
||||||
},
|
},
|
||||||
setup(props, {emit}) {
|
setup(_, { emit }) {
|
||||||
|
function isGranted(val: FG_Plugin.MenuLink) {
|
||||||
function isGranted(val: FG_Plugin.MenuLink) { return hasPermissions(val.permissions || [])};
|
return hasPermissions(val.permissions || []);
|
||||||
const title = computed(() =>
|
}
|
||||||
typeof props.entry.title === 'object' ? props.entry.title.value : props.entry.title
|
function getTitle(entry: FG_Plugin.MenuLink) {
|
||||||
);
|
return typeof entry.title === 'function' ? entry.title() : entry.title;
|
||||||
function addShortCut(val: FG_Plugin.MenuLink) {
|
|
||||||
emit('addShortCut', val)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isGranted, title, addShortCut};
|
function addShortCut(val: FG_Plugin.MenuLink) {
|
||||||
|
emit('addShortCut', val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isGranted, getTitle, addShortCut };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|