release v2.0.0 #4
|
@ -21,7 +21,7 @@ module.exports = configure(function (/* ctx */) {
|
|||
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// https://quasar.dev/quasar-cli/prefetch-feature
|
||||
// preFetch: true,
|
||||
|
|
|
@ -1,45 +1,25 @@
|
|||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import config from 'src/config';
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import config from '../config';
|
||||
import { Store } from 'vuex';
|
||||
import { StateInterface } from 'src/store';
|
||||
import { LocalStorage } from 'quasar';
|
||||
import { Notify } from 'quasar';
|
||||
import { LocalStorage, Notify } from 'quasar';
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||
import { UserSessionState } from 'src/plugins/user/store';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
export const setBaseUrl = (url: string) => {
|
||||
LocalStorage.set('baseURL', url);
|
||||
axios.defaults.baseURL = url;
|
||||
Notify.create({
|
||||
message: 'Serveraddresse gespeichert',
|
||||
position: 'bottom',
|
||||
caption: `${url}`,
|
||||
color: 'positive',
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
};
|
||||
const api = axios.create({
|
||||
baseURL: <string | undefined>LocalStorage.getItem('baseURL') || config.baseURL,
|
||||
});
|
||||
|
||||
export default boot<Store<StateInterface>>(({ Vue, store, router }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
Vue.prototype.$axios = axios;
|
||||
const baseURL = <string | undefined>LocalStorage.getItem('baseURL');
|
||||
if (baseURL) {
|
||||
axios.defaults.baseURL = baseURL;
|
||||
} else {
|
||||
axios.defaults.baseURL = config.baseURL;
|
||||
}
|
||||
export default boot<UserSessionState>(({ app, store, router }) => {
|
||||
/***
|
||||
* Intercept requests and insert Token if available
|
||||
*/
|
||||
axios.interceptors.request.use((config) => {
|
||||
const session = store.state.session.currentSession;
|
||||
api.interceptors.request.use((config) => {
|
||||
const session = store.state.sessions.currentSession;
|
||||
if (session?.token) {
|
||||
config.headers = { Authorization: 'Bearer ' + session.token };
|
||||
}
|
||||
|
@ -62,15 +42,39 @@ export default boot<Store<StateInterface>>(({ Vue, store, router }) => {
|
|||
) {
|
||||
return router.push({
|
||||
name: 'offline',
|
||||
query: { redirect: router.currentRoute.fullPath },
|
||||
query: { redirect: router.currentRoute.value.fullPath },
|
||||
});
|
||||
} else if (e.response && e.response.status == 401) {
|
||||
if (router.currentRoute.name !== 'login') return store.dispatch('session/clearCurrent');
|
||||
if (router.currentRoute.value.name !== 'login')
|
||||
return store.dispatch('session/clearCurrent');
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
});
|
||||
|
||||
export { axios };
|
||||
export { axios, api };
|
||||
|
||||
export const setBaseUrl = (url: string) => {
|
||||
LocalStorage.set('baseURL', url);
|
||||
axios.defaults.baseURL = url;
|
||||
Notify.create({
|
||||
message: 'Serveraddresse gespeichert',
|
||||
position: 'bottom',
|
||||
caption: `${url}`,
|
||||
color: 'positive',
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
};
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { boot } from 'quasar/wrappers';
|
||||
import { formatDateTime } from 'src/utils/datetime';
|
||||
|
||||
export default boot(({ Vue }) => {
|
||||
Vue.filter('date', formatDateTime);
|
||||
Vue.filter('time', (date: Date, seconds = false) => formatDateTime(date, false, true, seconds));
|
||||
Vue.filter('dateTime', (date: Date, seconds = false, weekday = false) =>
|
||||
formatDateTime(date, true, true, seconds, weekday)
|
||||
);
|
||||
});
|
|
@ -4,7 +4,7 @@ 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((/* { app, router, Vue ... } */) => {
|
||||
export default boot(() => {
|
||||
Loading.setDefaults({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { boot } from 'quasar/wrappers';
|
||||
import { StateInterface } from 'src/store';
|
||||
import { UserSessionState } from 'src/plugins/user/store';
|
||||
import { RouteRecord } from 'vue-router';
|
||||
import { Store } from 'vuex';
|
||||
|
||||
export default boot<Store<StateInterface>>(({ router, store }) => {
|
||||
export default boot<UserSessionState>(({ router, store }) => {
|
||||
router.beforeEach((to, from, next) => {
|
||||
const session = store.state.session.currentSession;
|
||||
const session = store.state.sessions.currentSession;
|
||||
|
||||
if (to.path == from.path) {
|
||||
return;
|
||||
|
@ -29,7 +28,7 @@ export default boot<Store<StateInterface>>(({ router, store }) => {
|
|||
if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
|
||||
return (<{ permissions: FG.Permission[] }>record.meta).permissions.every(
|
||||
(permission: string) => {
|
||||
return store.state.user.currentPermissions.includes(permission);
|
||||
return store.state.users.currentPermissions.includes(permission);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +40,7 @@ export default boot<Store<StateInterface>>(({ router, store }) => {
|
|||
next({ name: 'login', query: { redirect: to.fullPath } });
|
||||
}
|
||||
} else {
|
||||
if (to.name == 'login' && store.state.user.currentUser && !to.params['logout']) {
|
||||
if (to.name == 'login' && store.state.users.currentUser && !to.params['logout']) {
|
||||
// Called login while already logged in
|
||||
void next({ name: 'dashboard' });
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Notify } from 'quasar';
|
|||
|
||||
// "async" is optional;
|
||||
// more info on params: https://quasar.dev/quasar-cli/boot-files
|
||||
export default boot((/* { app, router, Vue ... } */) => {
|
||||
export default boot(() => {
|
||||
Notify.registerType('error', {
|
||||
color: 'negative',
|
||||
icon: 'mdi-alert-circle',
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { boot } from 'quasar/wrappers';
|
||||
import { Store } from 'vuex';
|
||||
import { StateInterface } from 'src/store';
|
||||
import { FG_Plugin } from 'src/plugins';
|
||||
import routes from 'src/router/routes';
|
||||
import { axios } from 'boot/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Router, RouteRecordRaw } from 'vue-router';
|
||||
import { UserSessionState } from 'src/plugins/user/store';
|
||||
|
||||
const config = {
|
||||
// Do not change required Modules !!
|
||||
|
@ -154,7 +154,7 @@ function loadPlugin(
|
|||
modules: string[],
|
||||
backendpromise: Promise<Backend | null>,
|
||||
plugins: FG_Plugin.Plugin[],
|
||||
store: Store<any>,
|
||||
store: Store<UserSessionState>,
|
||||
router: Router
|
||||
): FG_Plugin.LoadedPlugins {
|
||||
modules.forEach((requiredModule) => {
|
||||
|
@ -211,7 +211,7 @@ async function getBackend(): Promise<Backend | null> {
|
|||
|
||||
// "async" is optional;
|
||||
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
|
||||
export default boot<Store<StateInterface>>(({ router, store, app }) => {
|
||||
export default boot<UserSessionState>(({ router, store, app }) => {
|
||||
const plugins: FG_Plugin.Plugin[] = [];
|
||||
|
||||
const backendPromise = getBackend();
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { SessionStateInterface } from './session';
|
||||
import { UserStateInterface } from './user';
|
||||
|
||||
export interface UserSessionState {
|
||||
sessions: SessionStateInterface;
|
||||
users: UserStateInterface;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
|
||||
import { LoginData, LoginResponse } from 'src/plugins/user/models';
|
||||
import { StateInterface } from 'src/store';
|
||||
import { axios } from 'src/boot/axios';
|
||||
import { api } from 'src/boot/axios';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { Router } from 'src/router';
|
||||
import { LocalStorage } from 'quasar';
|
||||
import { UserSessionState } from '.';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export interface SessionInterface {
|
||||
export interface SessionStateInterface {
|
||||
currentSession?: FG.Session;
|
||||
sessions: FG.Session[];
|
||||
loading: boolean;
|
||||
|
@ -22,13 +22,13 @@ function loadCurrentSession() {
|
|||
return session;
|
||||
}
|
||||
|
||||
const state: SessionInterface = {
|
||||
const state: SessionStateInterface = {
|
||||
sessions: [],
|
||||
currentSession: loadCurrentSession() || undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
const mutations: MutationTree<SessionInterface> = {
|
||||
const mutations: MutationTree<SessionStateInterface> = {
|
||||
setCurrentSession(state, session: FG.Session) {
|
||||
LocalStorage.set('currentSession', session);
|
||||
state.currentSession = session;
|
||||
|
@ -51,19 +51,22 @@ const mutations: MutationTree<SessionInterface> = {
|
|||
},
|
||||
};
|
||||
|
||||
const actions: ActionTree<SessionInterface, StateInterface> = {
|
||||
const actions: ActionTree<SessionStateInterface, UserSessionState> = {
|
||||
/** Used to authenticate the user
|
||||
* Setting current Session, User and Permissions.
|
||||
* @param param0 Context
|
||||
* @param data Credentitals
|
||||
*/
|
||||
login({ commit, dispatch }, data: LoginData) {
|
||||
return axios
|
||||
login({ commit }, data: LoginData) {
|
||||
return api
|
||||
.post('/auth', data)
|
||||
.then(async (response: AxiosResponse<FG.Session>) => {
|
||||
response.data.expires = new Date(response.data.expires);
|
||||
commit('setCurrentSession', response.data);
|
||||
await dispatch('user/getCurrentUser', undefined, { root: true });
|
||||
.then((response: AxiosResponse<LoginResponse>) => {
|
||||
response.data.session.expires = new Date(response.data.session.expires);
|
||||
commit('setCurrentSession', response.data.session);
|
||||
commit('user/setCurrentUser', response.data.user, { root: true });
|
||||
commit('user/setCurrentPermissions', response.data.permissions, {
|
||||
root: true,
|
||||
});
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
return Promise.reject(error.response);
|
||||
|
@ -74,8 +77,8 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
|
|||
* Alias of deleteSession with current session as target
|
||||
*/
|
||||
logout({ dispatch, rootState }) {
|
||||
if (rootState.session.currentSession) {
|
||||
dispatch('deleteSession', rootState.session.currentSession.token).catch((error) => {
|
||||
if (rootState.sessions.currentSession) {
|
||||
dispatch('deleteSession', rootState.sessions.currentSession.token).catch((error) => {
|
||||
console.log(error);
|
||||
void dispatch('clearCurrent', false);
|
||||
});
|
||||
|
@ -88,10 +91,10 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
|
|||
*/
|
||||
deleteSession({ commit, dispatch, rootState }, token: string) {
|
||||
commit('setLoading', true);
|
||||
axios
|
||||
api
|
||||
.delete(`/auth/${token}`)
|
||||
.then(() => {
|
||||
if (token === rootState.session.currentSession?.token) {
|
||||
if (token === rootState.sessions.currentSession?.token) {
|
||||
void dispatch('clearCurrent', false);
|
||||
} else {
|
||||
dispatch('getSessions').catch((error) => {
|
||||
|
@ -110,23 +113,26 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
|
|||
* Clear current session and logged in user
|
||||
*/
|
||||
clearCurrent({ commit }, redirect = true) {
|
||||
void Router.push({
|
||||
name: 'login',
|
||||
query: redirect ? { redirect: Router.currentRoute.fullPath } : {},
|
||||
params: { logout: 'true' },
|
||||
}).then(() => {
|
||||
commit('clearCurrentSession');
|
||||
commit('user/clearCurrentUser', null, { root: true });
|
||||
// ensure also volatile store gets cleared by refreshing the site
|
||||
Router.go(0);
|
||||
});
|
||||
const router = useRouter();
|
||||
void router
|
||||
.push({
|
||||
name: 'login',
|
||||
query: redirect ? { redirect: router.currentRoute.value.fullPath } : {},
|
||||
params: { logout: 'true' },
|
||||
})
|
||||
.then(() => {
|
||||
commit('clearCurrentSession');
|
||||
commit('user/clearCurrentUser', null, { root: true });
|
||||
// ensure also volatile store gets cleared by refreshing the site
|
||||
router.go(0);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get all sessions from current User
|
||||
*/
|
||||
getSessions({ commit, state }) {
|
||||
commit('setLoading', true);
|
||||
axios
|
||||
api
|
||||
.get('/auth')
|
||||
.then((response: AxiosResponse<FG.Session[]>) => {
|
||||
response.data.forEach((session) => {
|
||||
|
@ -149,7 +155,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
|
|||
},
|
||||
updateSession({ commit, state }, data: { lifetime: number; token: string }) {
|
||||
commit('setLoading', true);
|
||||
axios
|
||||
api
|
||||
.put(`auth/${data.token}`, { value: data.lifetime })
|
||||
.then((response: AxiosResponse<FG.Session>) => {
|
||||
response.data.expires = new Date(response.data.expires);
|
||||
|
@ -164,16 +170,16 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
|
|||
console.log('updateSession', data);
|
||||
},
|
||||
requestPasswordReset({}, data) {
|
||||
return axios.post('/auth/reset', data);
|
||||
return api.post('/auth/reset', data);
|
||||
},
|
||||
resetPassword({}, data) {
|
||||
return axios.post('/auth/reset', data).catch((error: AxiosError) => {
|
||||
return api.post('/auth/reset', data).catch((error: AxiosError) => {
|
||||
return Promise.reject(error.response);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const getters: GetterTree<SessionInterface, StateInterface> = {
|
||||
const getters: GetterTree<SessionStateInterface, UserSessionState> = {
|
||||
currentSession(state) {
|
||||
return state.currentSession;
|
||||
},
|
||||
|
@ -185,7 +191,7 @@ const getters: GetterTree<SessionInterface, StateInterface> = {
|
|||
},
|
||||
};
|
||||
|
||||
const sessions: Module<SessionInterface, StateInterface> = {
|
||||
const sessions: Module<SessionStateInterface, UserSessionState> = {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
|
||||
import { StateInterface } from 'src/store';
|
||||
import { axios } from 'boot/axios';
|
||||
import { api } from 'boot/axios';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { SessionStorage } from 'quasar';
|
||||
import { CurrentUserResponse } from 'src/plugins/user/models';
|
||||
import { UserSessionState } from '.';
|
||||
|
||||
export interface UserStateInterface {
|
||||
currentUser?: FG.User;
|
||||
|
@ -76,12 +76,12 @@ const mutations: MutationTree<UserStateInterface> = {
|
|||
},
|
||||
};
|
||||
|
||||
const actions: ActionTree<UserStateInterface, StateInterface> = {
|
||||
const actions: ActionTree<UserStateInterface, UserSessionState> = {
|
||||
getCurrentUser({ commit, rootState }) {
|
||||
if (rootState.session.currentSession) {
|
||||
if (rootState.sessions.currentSession) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
.get(`/users/${rootState.session.currentSession.userid}`)
|
||||
api
|
||||
.get(`/users/${rootState.sessions.currentSession.userid}`)
|
||||
.then((response: AxiosResponse<CurrentUserResponse>) => {
|
||||
commit('setCurrentUser', response.data);
|
||||
commit('setCurrentPermissions', response.data.permissions);
|
||||
|
@ -99,7 +99,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
getUsers({ commit }) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.get('/users')
|
||||
.then((response: AxiosResponse<FG.User[]>) => {
|
||||
response.data.forEach((user) => {
|
||||
|
@ -117,7 +117,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
updateUser({ commit, state, dispatch }, data: FG.User) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.put(`/users/${data.userid}`, data)
|
||||
.then(() => {
|
||||
if (state.currentUser && state.currentUser.userid === data.userid)
|
||||
|
@ -136,7 +136,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
commit('setLoading');
|
||||
const formData = new FormData();
|
||||
formData.append('file', payload.file);
|
||||
return axios
|
||||
return api
|
||||
.post(`/users/${payload.user.userid}/avatar`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
|
@ -152,7 +152,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
setUser({ commit, state, dispatch }, data: FG.User) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.post('users', data)
|
||||
.then(() => {
|
||||
if (state.currentUser && state.currentUser.userid === data.userid)
|
||||
|
@ -170,7 +170,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
getRoles({ commit, state }, force = false) {
|
||||
if (!force && state.roles.length > 0) return;
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.get('/roles')
|
||||
.then((response: AxiosResponse<FG.Role[]>) => {
|
||||
commit('setRoles', response.data);
|
||||
|
@ -180,7 +180,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
updateRole({ commit }, data: FG.Role) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.put(`/roles/${data.id}`, data)
|
||||
.then(() => {
|
||||
commit('updateRole', data);
|
||||
|
@ -192,7 +192,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
newRole({ commit }, data: FG.Role) {
|
||||
commit('setLoading');
|
||||
return axios
|
||||
return api
|
||||
.post('/roles', data)
|
||||
.then((response: AxiosResponse<FG.Role>) => {
|
||||
commit('addRole', response.data);
|
||||
|
@ -205,7 +205,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
|
||||
deleteRole({ commit, state }, data: FG.Role) {
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.delete(`/roles/${data.id}`)
|
||||
.then(() => {
|
||||
commit(
|
||||
|
@ -221,7 +221,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
getPermissions({ commit, state }, force = false) {
|
||||
if (!force && state.permissions.length > 0) return;
|
||||
commit('setLoading');
|
||||
axios
|
||||
api
|
||||
.get('/roles/permissions')
|
||||
.then((response: AxiosResponse<FG.Role[]>) => {
|
||||
commit('setPermissions', response.data);
|
||||
|
@ -232,7 +232,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const user = <FG.User | undefined>getters['getUser'](data.userid);
|
||||
if (user === undefined || data.force === true) {
|
||||
return axios.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => {
|
||||
return api.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => {
|
||||
commit('setUser', response.data);
|
||||
return response.data;
|
||||
});
|
||||
|
@ -242,7 +242,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
|
|||
},
|
||||
};
|
||||
|
||||
const getters: GetterTree<UserStateInterface, StateInterface> = {
|
||||
const getters: GetterTree<UserStateInterface, UserSessionState> = {
|
||||
getUser: (state) => (userid: string) => {
|
||||
const user = state.users.filter((usr) => usr.userid === userid);
|
||||
return user.length > 0 ? user[0] : undefined;
|
||||
|
@ -264,7 +264,7 @@ const getters: GetterTree<UserStateInterface, StateInterface> = {
|
|||
},
|
||||
};
|
||||
|
||||
const userStore: Module<UserStateInterface, StateInterface> = {
|
||||
const userStore: Module<UserStateInterface, UserSessionState> = {
|
||||
namespaced: true,
|
||||
actions,
|
||||
getters,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// 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 {
|
||||
store: true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue