From dd875d504d6f1176110df66a027e8bda5a4e0267 Mon Sep 17 00:00:00 2001 From: Christian Bosdorf Date: Wed, 22 Apr 2020 17:00:07 +0200 Subject: [PATCH] :recycle: Refactor frontend * :recycle: Migrate to latest Vuetify 2.x API, for details see migration guide (https://vuetifyjs.com/en/getting-started/releases-and-migrations/#migration-guide) * :recycle: Migrate to latest Vee-Validate 3.x API, for details see migration guide (https://logaretm.github.io/vee-validate/migration.html#migration-guide) * :heavy_plus_sign: Refactor state management with vuex-module-decorators (https://github.com/championswimmer/vuex-module-decorators), which is similar in style to vue-class-components * :sparkles: Add configuration for eslint (+prettier, +typescript) * :art: Reformat code with `eslint --fix` * :pushpin: Upgrade/add relevant dependencies Thanks @Nonameentered and @Biskit1943 for reviewing the code and fixing some errors! --- .../frontend/.browserslistrc | 2 + .../frontend/.eslintrc.js | 52 +++ .../frontend/.prettierrc.js | 10 + .../frontend/jest.config.js | 4 + .../frontend/package.json | 98 +++--- .../frontend/public/index.html | 19 +- .../frontend/src/App.vue | 50 +-- .../frontend/src/api.ts | 18 +- .../frontend/src/component-hooks.ts | 8 +- .../src/components/NotificationsManager.vue | 101 +++--- .../src/components/RouterComponent.vue | 7 +- .../frontend/src/components/UploadButton.vue | 46 +-- .../frontend/src/env.ts | 6 +- .../frontend/src/interfaces/index.ts | 36 +- .../frontend/src/main.ts | 21 +- .../frontend/src/plugins/vee-validate.ts | 4 - .../frontend/src/plugins/vuetify.ts | 13 +- .../frontend/src/registerServiceWorker.ts | 24 +- .../frontend/src/router.ts | 97 ------ .../frontend/src/router/index.ts | 123 +++++++ .../frontend/src/shims-tsx.d.ts | 2 +- .../frontend/src/shims-vue.d.ts | 4 +- .../frontend/src/store/admin/actions.ts | 60 ---- .../frontend/src/store/admin/getters.ts | 18 - .../frontend/src/store/admin/index.ts | 15 - .../frontend/src/store/admin/mutations.ts | 20 -- .../frontend/src/store/admin/state.ts | 5 - .../frontend/src/store/index.ts | 26 +- .../frontend/src/store/main/actions.ts | 173 ---------- .../frontend/src/store/main/getters.ts | 29 -- .../frontend/src/store/main/index.ts | 21 -- .../frontend/src/store/main/mutations.ts | 43 --- .../frontend/src/store/main/state.ts | 17 - .../frontend/src/store/modules/admin.ts | 83 +++++ .../frontend/src/store/modules/main.ts | 253 ++++++++++++++ .../frontend/src/store/state.ts | 5 - .../frontend/src/utils.ts | 5 - .../frontend/src/utils/index.ts | 5 + .../frontend/src/utils/store-accessor.ts | 20 ++ .../frontend/src/views/Login.vue | 70 ++-- .../frontend/src/views/PasswordRecovery.vue | 122 ++++--- .../frontend/src/views/ResetPassword.vue | 176 ++++++---- .../frontend/src/views/main/Dashboard.vue | 27 +- .../frontend/src/views/main/Main.vue | 304 +++++++++-------- .../frontend/src/views/main/Start.vue | 52 ++- .../frontend/src/views/main/admin/Admin.vue | 37 +- .../src/views/main/admin/AdminUsers.vue | 124 ++++--- .../src/views/main/admin/CreateUser.vue | 224 ++++++++---- .../src/views/main/admin/EditUser.vue | 320 ++++++++++-------- .../src/views/main/profile/UserProfile.vue | 45 ++- .../views/main/profile/UserProfileEdit.vue | 168 +++++---- .../main/profile/UserProfileEditPassword.vue | 175 ++++++---- .../frontend/tests/unit/upload-button.spec.ts | 5 + .../frontend/tsconfig.json | 24 +- .../frontend/tslint.json | 19 -- .../frontend/vue.config.js | 20 +- 56 files changed, 1882 insertions(+), 1573 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/frontend/.browserslistrc create mode 100644 {{cookiecutter.project_slug}}/frontend/.eslintrc.js create mode 100644 {{cookiecutter.project_slug}}/frontend/.prettierrc.js create mode 100644 {{cookiecutter.project_slug}}/frontend/jest.config.js delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/router.ts create mode 100644 {{cookiecutter.project_slug}}/frontend/src/router/index.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/main/index.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/main/state.ts create mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/modules/admin.ts create mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/modules/main.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/store/state.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/src/utils.ts create mode 100644 {{cookiecutter.project_slug}}/frontend/src/utils/index.ts create mode 100644 {{cookiecutter.project_slug}}/frontend/src/utils/store-accessor.ts delete mode 100644 {{cookiecutter.project_slug}}/frontend/tslint.json diff --git a/{{cookiecutter.project_slug}}/frontend/.browserslistrc b/{{cookiecutter.project_slug}}/frontend/.browserslistrc new file mode 100644 index 0000000000..d6471a38cc --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +last 2 versions diff --git a/{{cookiecutter.project_slug}}/frontend/.eslintrc.js b/{{cookiecutter.project_slug}}/frontend/.eslintrc.js new file mode 100644 index 0000000000..c5d3cea483 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/.eslintrc.js @@ -0,0 +1,52 @@ +const OFF = 0, + WARN = 1, + ERROR = 2; + +module.exports = { + root: true, + env: { + node: true, + }, + ignorePatterns: [ + "!.eslintrc.js", + "!.prettierrc.js", + "node_modules/", + "shims-tsx.d.ts", + "shims-vue.d.ts" + ], + extends: [ + "plugin:vue/recommended", + "eslint:recommended", + "@vue/typescript/recommended", + "@vue/prettier", + "@vue/prettier/@typescript-eslint", + ], + parserOptions: { + ecmaVersion: 2020, + }, + rules: { + "no-console": process.env.NODE_ENV === "production" ? ERROR : OFF, + "no-debugger": process.env.NODE_ENV === "production" ? ERROR : OFF, + "@typescript-eslint/interface-name-prefix": [ + WARN, + { + prefixWithI: "always", + }, + ], + "@typescript-eslint/no-unused-vars": [ + ERROR, + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, + overrides: [ + { + files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"], + env: { + jest: true, + }, + }, + ], +}; diff --git a/{{cookiecutter.project_slug}}/frontend/.prettierrc.js b/{{cookiecutter.project_slug}}/frontend/.prettierrc.js new file mode 100644 index 0000000000..b6746e0766 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/.prettierrc.js @@ -0,0 +1,10 @@ +module.exports = { + printWidth: 88, + tabWidth: 2, + tabs: false, + semi: true, + singleQuote: false, + trailingComma: "all", + arrowParens: "always", + vueIndentScriptAndStyle: true, +}; diff --git a/{{cookiecutter.project_slug}}/frontend/jest.config.js b/{{cookiecutter.project_slug}}/frontend/jest.config.js new file mode 100644 index 0000000000..1342125c86 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel", + transformIgnorePatterns: ['/node_modules/(?!@mdi|vuetify)'], +}; diff --git a/{{cookiecutter.project_slug}}/frontend/package.json b/{{cookiecutter.project_slug}}/frontend/package.json index a83c616980..ed2e246535 100644 --- a/{{cookiecutter.project_slug}}/frontend/package.json +++ b/{{cookiecutter.project_slug}}/frontend/package.json @@ -9,66 +9,46 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "@babel/polyfill": "^7.2.5", - "axios": "^0.18.0", - "core-js": "^3.4.3", - "register-service-worker": "^1.0.0", - "typesafe-vuex": "^3.1.1", - "vee-validate": "^2.1.7", - "vue": "^2.5.22", - "vue-class-component": "^6.0.0", - "vue-property-decorator": "^7.3.0", - "vue-router": "^3.0.2", - "vuetify": "^1.4.4", - "vuex": "^3.1.0" + "@mdi/font": "^3.6.95", + "axios": "^0.19.2", + "core-js": "^3.6.4", + "http-status-codes": "^1.4.0", + "register-service-worker": "^1.6.2", + "roboto-fontface": "*", + "vee-validate": "^3.2.5", + "vue": "^2.6.11", + "vue-class-component": "^7.2.2", + "vue-property-decorator": "^8.3.0", + "vue-router": "^3.1.6", + "vuetify": "^2.2.20", + "vuex": "^3.1.2" }, "devDependencies": { - "@types/jest": "^23.3.13", - "@vue/cli-plugin-babel": "^4.1.1", - "@vue/cli-plugin-pwa": "^4.1.1", - "@vue/cli-plugin-typescript": "^4.1.1", - "@vue/cli-plugin-unit-jest": "^4.1.1", - "@vue/cli-service": "^4.1.1", - "@vue/test-utils": "^1.0.0-beta.28", - "babel-core": "7.0.0-bridge.0", - "ts-jest": "^23.10.5", - "typescript": "^3.2.4", - "vue-cli-plugin-vuetify": "^2.0.2", - "vue-template-compiler": "^2.5.22" - }, - "postcss": { - "plugins": { - "autoprefixer": {} - } - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not ie <= 10" - ], - "jest": { - "moduleFileExtensions": [ - "js", - "jsx", - "json", - "vue", - "ts", - "tsx" - ], - "transform": { - "^.+\\.vue$": "vue-jest", - ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", - "^.+\\.tsx?$": "ts-jest" - }, - "moduleNameMapper": { - "^@/(.*)$": "/src/$1" - }, - "snapshotSerializers": [ - "jest-serializer-vue" - ], - "testMatch": [ - "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" - ], - "testURL": "http://localhost/" + "@types/jest": "^24.0.19", + "@typescript-eslint/eslint-plugin": "^2.18.0", + "@typescript-eslint/parser": "^2.18.0", + "@vue/cli-plugin-babel": "~4.2.0", + "@vue/cli-plugin-eslint": "~4.2.0", + "@vue/cli-plugin-pwa": "~4.2.0", + "@vue/cli-plugin-router": "~4.2.0", + "@vue/cli-plugin-typescript": "~4.2.0", + "@vue/cli-plugin-unit-jest": "~4.2.0", + "@vue/cli-plugin-vuex": "~4.2.0", + "@vue/cli-service": "~4.2.0", + "@vue/eslint-config-prettier": "^6.0.0", + "@vue/eslint-config-typescript": "^5.0.1", + "@vue/test-utils": "1.0.0-beta.31", + "eslint": "^6.7.2", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-vue": "^6.1.2", + "prettier": "^1.19.1", + "sass": "^1.25.0", + "sass-loader": "^8.0.2", + "typescript": "^3.8.3", + "vue-cli-plugin-vuetify": "~2.0.5", + "vue-template-compiler": "^2.6.11", + "vuetify-loader": "^1.3.0", + "vuex-module-decorators": "^0.16.1", + "webpack": "^4.42.1" } } diff --git a/{{cookiecutter.project_slug}}/frontend/public/index.html b/{{cookiecutter.project_slug}}/frontend/public/index.html index cad5aa7efa..a6699c4cef 100644 --- a/{{cookiecutter.project_slug}}/frontend/public/index.html +++ b/{{cookiecutter.project_slug}}/frontend/public/index.html @@ -1,19 +1,18 @@ - - - - - <%= VUE_APP_NAME %> - - - - + + + + + <%= htmlWebpackPlugin.options.title %>
diff --git a/{{cookiecutter.project_slug}}/frontend/src/App.vue b/{{cookiecutter.project_slug}}/frontend/src/App.vue index 795a97c955..ce9c08c8e9 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/App.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/App.vue @@ -1,16 +1,20 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/api.ts b/{{cookiecutter.project_slug}}/frontend/src/api.ts index c24712b872..c7c6cb5794 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/api.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/api.ts @@ -1,6 +1,6 @@ -import axios from 'axios'; -import { apiUrl } from '@/env'; -import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from './interfaces'; +import axios from "axios"; +import { apiUrl } from "@/env"; +import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from "./interfaces"; function authHeaders(token: string) { return { @@ -13,8 +13,8 @@ function authHeaders(token: string) { export const api = { async logInGetToken(username: string, password: string) { const params = new URLSearchParams(); - params.append('username', username); - params.append('password', password); + params.append("username", username); + params.append("password", password); return axios.post(`${apiUrl}/api/v1/login/access-token`, params); }, @@ -22,7 +22,11 @@ export const api = { return axios.get(`${apiUrl}/api/v1/users/me`, authHeaders(token)); }, async updateMe(token: string, data: IUserProfileUpdate) { - return axios.put(`${apiUrl}/api/v1/users/me`, data, authHeaders(token)); + return axios.put( + `${apiUrl}/api/v1/users/me`, + data, + authHeaders(token), + ); }, async getUsers(token: string) { return axios.get(`${apiUrl}/api/v1/users/`, authHeaders(token)); @@ -38,7 +42,7 @@ export const api = { }, async resetPassword(password: string, token: string) { return axios.post(`${apiUrl}/api/v1/reset-password/`, { - new_password: password, + new_password: password, // eslint-disable-line @typescript-eslint/camelcase token, }); }, diff --git a/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts b/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts index cefdc537bb..53041834d3 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts @@ -1,8 +1,8 @@ -import Component from 'vue-class-component'; +import Component from "vue-class-component"; // Register the router hooks with their names Component.registerHooks([ - 'beforeRouteEnter', - 'beforeRouteLeave', - 'beforeRouteUpdate', // for vue-router 2.2+ + "beforeRouteEnter", + "beforeRouteLeave", + "beforeRouteUpdate", // for vue-router 2.2+ ]); diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue b/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue index 6fcffdb789..e8bbdc2874 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue @@ -1,77 +1,80 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue b/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue index ed986a6fda..950402d0ef 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue @@ -3,9 +3,8 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue b/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue index 8902e949e9..20566a8d8b 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue @@ -1,34 +1,40 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/env.ts b/{{cookiecutter.project_slug}}/frontend/src/env.ts index b3387e69bc..a6d80fee20 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/env.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/env.ts @@ -1,10 +1,10 @@ const env = process.env.VUE_APP_ENV; -let envApiUrl = ''; +let envApiUrl = ""; -if (env === 'production') { +if (env === "production") { envApiUrl = `https://${process.env.VUE_APP_DOMAIN_PROD}`; -} else if (env === 'staging') { +} else if (env === "staging") { envApiUrl = `https://${process.env.VUE_APP_DOMAIN_STAG}`; } else { envApiUrl = `http://${process.env.VUE_APP_DOMAIN_DEV}`; diff --git a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts b/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts index a1b93403cf..14414d0cd2 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts @@ -1,23 +1,29 @@ export interface IUserProfile { - email: string; - is_active: boolean; - is_superuser: boolean; - full_name: string; - id: number; + email: string; + is_active: boolean; + is_superuser: boolean; + full_name: string; + id: number; } export interface IUserProfileUpdate { - email?: string; - full_name?: string; - password?: string; - is_active?: boolean; - is_superuser?: boolean; + email?: string; + full_name?: string; + password?: string; + is_active?: boolean; + is_superuser?: boolean; } export interface IUserProfileCreate { - email: string; - full_name?: string; - password?: string; - is_active?: boolean; - is_superuser?: boolean; + email: string; + full_name?: string; + password?: string; + is_active?: boolean; + is_superuser?: boolean; +} + +export interface IAppNotification { + content: string; + color?: string; + showProgress?: boolean; } diff --git a/{{cookiecutter.project_slug}}/frontend/src/main.ts b/{{cookiecutter.project_slug}}/frontend/src/main.ts index a844b1eab8..6df6dd7db6 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/main.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/main.ts @@ -1,19 +1,16 @@ -import '@babel/polyfill'; -// Import Component hooks before component definitions -import './component-hooks'; -import Vue from 'vue'; -import './plugins/vuetify'; -import './plugins/vee-validate'; -import App from './App.vue'; -import router from './router'; -import store from '@/store'; -import './registerServiceWorker'; -import 'vuetify/dist/vuetify.min.css'; +import "./component-hooks"; +import Vue from "vue"; +import App from "./App.vue"; +import "./registerServiceWorker"; +import router from "./router"; +import store from "./store"; +import vuetify from "./plugins/vuetify"; Vue.config.productionTip = false; new Vue({ router, store, + vuetify, render: (h) => h(App), -}).$mount('#app'); +}).$mount("#app"); diff --git a/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts b/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts deleted file mode 100644 index 9c4238f2f7..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Vue from 'vue'; -import VeeValidate from 'vee-validate'; - -Vue.use(VeeValidate); diff --git a/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts b/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts index 8fdfce3a4a..f9777af74a 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts @@ -1,6 +1,11 @@ -import Vue from 'vue'; -import Vuetify from 'vuetify'; +import "@mdi/font/css/materialdesignicons.css"; +import Vue from "vue"; +import Vuetify from "vuetify/lib"; -Vue.use(Vuetify, { - iconfont: 'md', +Vue.use(Vuetify); + +export default new Vuetify({ + icons: { + iconfont: "mdi", + }, }); diff --git a/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts b/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts index d3db583898..84c7707730 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts @@ -1,26 +1,32 @@ -/* tslint:disable:no-console */ +/* eslint-disable no-console */ -import { register } from 'register-service-worker'; +import { register } from "register-service-worker"; -if (process.env.NODE_ENV === 'production') { +if (process.env.NODE_ENV === "production") { register(`${process.env.BASE_URL}service-worker.js`, { ready() { console.log( - 'App is being served from cache by a service worker.\n' + - 'For more details, visit https://goo.gl/AFskqB', + "App is being served from cache by a service worker.\n" + + "For more details, visit https://goo.gl/AFskqB", ); }, + registered() { + console.log("Service worker has been registered."); + }, cached() { - console.log('Content has been cached for offline use.'); + console.log("Content has been cached for offline use."); + }, + updatefound() { + console.log("New content is downloading."); }, updated() { - console.log('New content is available; please refresh.'); + console.log("New content is available; please refresh."); }, offline() { - console.log('No internet connection found. App is running in offline mode.'); + console.log("No internet connection found. App is running in offline mode."); }, error(error) { - console.error('Error during service worker registration:', error); + console.error("Error during service worker registration:", error); }, }); } diff --git a/{{cookiecutter.project_slug}}/frontend/src/router.ts b/{{cookiecutter.project_slug}}/frontend/src/router.ts deleted file mode 100644 index b649c173ad..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Vue from 'vue'; -import Router from 'vue-router'; - -import RouterComponent from './components/RouterComponent.vue'; - -Vue.use(Router); - -export default new Router({ - mode: 'history', - base: process.env.BASE_URL, - routes: [ - { - path: '/', - component: () => import(/* webpackChunkName: "start" */ './views/main/Start.vue'), - children: [ - { - path: 'login', - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'), - }, - { - path: 'recover-password', - component: () => import(/* webpackChunkName: "recover-password" */ './views/PasswordRecovery.vue'), - }, - { - path: 'reset-password', - component: () => import(/* webpackChunkName: "reset-password" */ './views/ResetPassword.vue'), - }, - { - path: 'main', - component: () => import(/* webpackChunkName: "main" */ './views/main/Main.vue'), - children: [ - { - path: 'dashboard', - component: () => import(/* webpackChunkName: "main-dashboard" */ './views/main/Dashboard.vue'), - }, - { - path: 'profile', - component: RouterComponent, - redirect: 'profile/view', - children: [ - { - path: 'view', - component: () => import( - /* webpackChunkName: "main-profile" */ './views/main/profile/UserProfile.vue'), - }, - { - path: 'edit', - component: () => import( - /* webpackChunkName: "main-profile-edit" */ './views/main/profile/UserProfileEdit.vue'), - }, - { - path: 'password', - component: () => import( - /* webpackChunkName: "main-profile-password" */ './views/main/profile/UserProfileEditPassword.vue'), - }, - ], - }, - { - path: 'admin', - component: () => import(/* webpackChunkName: "main-admin" */ './views/main/admin/Admin.vue'), - redirect: 'admin/users/all', - children: [ - { - path: 'users', - redirect: 'users/all', - }, - { - path: 'users/all', - component: () => import( - /* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'), - }, - { - path: 'users/edit/:id', - name: 'main-admin-users-edit', - component: () => import( - /* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'), - }, - { - path: 'users/create', - name: 'main-admin-users-create', - component: () => import( - /* webpackChunkName: "main-admin-users-create" */ './views/main/admin/CreateUser.vue'), - }, - ], - }, - ], - }, - ], - }, - { - path: '/*', redirect: '/', - }, - ], -}); diff --git a/{{cookiecutter.project_slug}}/frontend/src/router/index.ts b/{{cookiecutter.project_slug}}/frontend/src/router/index.ts new file mode 100644 index 0000000000..57b7ad007b --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/router/index.ts @@ -0,0 +1,123 @@ +import Vue from "vue"; +import VueRouter from "vue-router"; + +import RouterComponent from "@/components/RouterComponent.vue"; + +Vue.use(VueRouter); + +export default new VueRouter({ + mode: "history", + base: process.env.BASE_URL, + routes: [ + { + path: "/", + component: () => import(/* webpackChunkName: "start" */ "@/views/main/Start.vue"), + children: [ + { + path: "login", + // route level code-splitting + // this generates a separate chunk (about.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import(/* webpackChunkName: "login" */ "@/views/Login.vue"), + }, + { + path: "recover-password", + component: () => + import( + /* webpackChunkName: "recover-password" */ "@/views/PasswordRecovery.vue" + ), + }, + { + path: "reset-password", + component: () => + import( + /* webpackChunkName: "reset-password" */ "@/views/ResetPassword.vue" + ), + }, + { + path: "main", + component: () => + import(/* webpackChunkName: "main" */ "@/views/main/Main.vue"), + children: [ + { + path: "dashboard", + component: () => + import( + /* webpackChunkName: "main-dashboard" */ "@/views/main/Dashboard.vue" + ), + }, + { + path: "profile", + component: RouterComponent, + redirect: "profile/view", + children: [ + { + path: "view", + component: () => + import( + /* webpackChunkName: "main-profile" */ "@/views/main/profile/UserProfile.vue" + ), + }, + { + path: "edit", + component: () => + import( + /* webpackChunkName: "main-profile-edit" */ "@/views/main/profile/UserProfileEdit.vue" + ), + }, + { + path: "password", + component: () => + import( + /* webpackChunkName: "main-profile-password" */ "@/views/main/profile/UserProfileEditPassword.vue" + ), + }, + ], + }, + { + path: "admin", + component: () => + import( + /* webpackChunkName: "main-admin" */ "@/views/main/admin/Admin.vue" + ), + redirect: "admin/users/all", + children: [ + { + path: "users", + redirect: "users/all", + }, + { + path: "users/all", + component: () => + import( + /* webpackChunkName: "main-admin-users" */ "@/views/main/admin/AdminUsers.vue" + ), + }, + { + path: "users/edit/:id", + name: "main-admin-users-edit", + component: () => + import( + /* webpackChunkName: "main-admin-users-edit" */ "@/views/main/admin/EditUser.vue" + ), + }, + { + path: "users/create", + name: "main-admin-users-create", + component: () => + import( + /* webpackChunkName: "main-admin-users-create" */ "@/views/main/admin/CreateUser.vue" + ), + }, + ], + }, + ], + }, + ], + }, + { + path: "/*", + redirect: "/", + }, + ], +}); diff --git a/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts b/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts index 3b88b58292..2bcdf9fbcb 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts @@ -1,4 +1,4 @@ -import Vue, { VNode } from 'vue'; +import Vue, { VNode } from "vue"; declare global { namespace JSX { diff --git a/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts b/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts index 8f6f410263..0660bd67a5 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts @@ -1,4 +1,4 @@ -declare module '*.vue' { - import Vue from 'vue'; +declare module "*.vue" { + import Vue from "vue"; export default Vue; } diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts deleted file mode 100644 index 125a08e6fd..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { api } from '@/api'; -import { ActionContext } from 'vuex'; -import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces'; -import { State } from '../state'; -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { commitSetUsers, commitSetUser } from './mutations'; -import { dispatchCheckApiError } from '../main/actions'; -import { commitAddNotification, commitRemoveNotification } from '../main/mutations'; - -type MainContext = ActionContext; - -export const actions = { - async actionGetUsers(context: MainContext) { - try { - const response = await api.getUsers(context.rootState.main.token); - if (response) { - commitSetUsers(context, response.data); - } - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionUpdateUser(context: MainContext, payload: { id: number, user: IUserProfileUpdate }) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.updateUser(context.rootState.main.token, payload.id, payload.user), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUser(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'User successfully updated', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionCreateUser(context: MainContext, payload: IUserProfileCreate) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.createUser(context.rootState.main.token, payload), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUser(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'User successfully created', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, -}; - -const { dispatch } = getStoreAccessors(''); - -export const dispatchCreateUser = dispatch(actions.actionCreateUser); -export const dispatchGetUsers = dispatch(actions.actionGetUsers); -export const dispatchUpdateUser = dispatch(actions.actionUpdateUser); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts deleted file mode 100644 index c5832ef449..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const getters = { - adminUsers: (state: AdminState) => state.users, - adminOneUser: (state: AdminState) => (userId: number) => { - const filteredUsers = state.users.filter((user) => user.id === userId); - if (filteredUsers.length > 0) { - return { ...filteredUsers[0] }; - } - }, -}; - -const { read } = getStoreAccessors(''); - -export const readAdminOneUser = read(getters.adminOneUser); -export const readAdminUsers = read(getters.adminUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts deleted file mode 100644 index dcaf6abbd1..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mutations } from './mutations'; -import { getters } from './getters'; -import { actions } from './actions'; -import { AdminState } from './state'; - -const defaultState: AdminState = { - users: [], -}; - -export const adminModule = { - state: defaultState, - mutations, - actions, - getters, -}; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts deleted file mode 100644 index dea471d4e7..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IUserProfile } from '@/interfaces'; -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const mutations = { - setUsers(state: AdminState, payload: IUserProfile[]) { - state.users = payload; - }, - setUser(state: AdminState, payload: IUserProfile) { - const users = state.users.filter((user: IUserProfile) => user.id !== payload.id); - users.push(payload); - state.users = users; - }, -}; - -const { commit } = getStoreAccessors(''); - -export const commitSetUser = commit(mutations.setUser); -export const commitSetUsers = commit(mutations.setUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts deleted file mode 100644 index 8dfefe2f99..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IUserProfile } from '@/interfaces'; - -export interface AdminState { - users: IUserProfile[]; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/index.ts index 1089971525..06aa075796 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/store/index.ts +++ b/{{cookiecutter.project_slug}}/frontend/src/store/index.ts @@ -1,19 +1,17 @@ -import Vue from 'vue'; -import Vuex, { StoreOptions } from 'vuex'; - -import { mainModule } from './main'; -import { State } from './state'; -import { adminModule } from './admin'; +import Vue from "vue"; +import Vuex, { Store } from "vuex"; +import { initializeStores, modules } from "@/utils/store-accessor"; Vue.use(Vuex); -const storeOptions: StoreOptions = { - modules: { - main: mainModule, - admin: adminModule, - }, -}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const initializer = (store: Store) => initializeStores(store); + +export const plugins = [initializer]; -export const store = new Vuex.Store(storeOptions); +export * from "@/utils/store-accessor"; -export default store; +export default new Store({ + plugins, + modules, +}); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts deleted file mode 100644 index d02c06d53c..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { api } from '@/api'; -import router from '@/router'; -import { getLocalToken, removeLocalToken, saveLocalToken } from '@/utils'; -import { AxiosError } from 'axios'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { ActionContext } from 'vuex'; -import { State } from '../state'; -import { - commitAddNotification, - commitRemoveNotification, - commitSetLoggedIn, - commitSetLogInError, - commitSetToken, - commitSetUserProfile, -} from './mutations'; -import { AppNotification, MainState } from './state'; - -type MainContext = ActionContext; - -export const actions = { - async actionLogIn(context: MainContext, payload: { username: string; password: string }) { - try { - const response = await api.logInGetToken(payload.username, payload.password); - const token = response.data.access_token; - if (token) { - saveLocalToken(token); - commitSetToken(context, token); - commitSetLoggedIn(context, true); - commitSetLogInError(context, false); - await dispatchGetUserProfile(context); - await dispatchRouteLoggedIn(context); - commitAddNotification(context, { content: 'Logged in', color: 'success' }); - } else { - await dispatchLogOut(context); - } - } catch (err) { - commitSetLogInError(context, true); - await dispatchLogOut(context); - } - }, - async actionGetUserProfile(context: MainContext) { - try { - const response = await api.getMe(context.state.token); - if (response.data) { - commitSetUserProfile(context, response.data); - } - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionUpdateUserProfile(context: MainContext, payload) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.updateMe(context.state.token, payload), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUserProfile(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Profile successfully updated', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionCheckLoggedIn(context: MainContext) { - if (!context.state.isLoggedIn) { - let token = context.state.token; - if (!token) { - const localToken = getLocalToken(); - if (localToken) { - commitSetToken(context, localToken); - token = localToken; - } - } - if (token) { - try { - const response = await api.getMe(token); - commitSetLoggedIn(context, true); - commitSetUserProfile(context, response.data); - } catch (error) { - await dispatchRemoveLogIn(context); - } - } else { - await dispatchRemoveLogIn(context); - } - } - }, - async actionRemoveLogIn(context: MainContext) { - removeLocalToken(); - commitSetToken(context, ''); - commitSetLoggedIn(context, false); - }, - async actionLogOut(context: MainContext) { - await dispatchRemoveLogIn(context); - await dispatchRouteLogOut(context); - }, - async actionUserLogOut(context: MainContext) { - await dispatchLogOut(context); - commitAddNotification(context, { content: 'Logged out', color: 'success' }); - }, - actionRouteLogOut(context: MainContext) { - if (router.currentRoute.path !== '/login') { - router.push('/login'); - } - }, - async actionCheckApiError(context: MainContext, payload: AxiosError) { - if (payload.response!.status === 401) { - await dispatchLogOut(context); - } - }, - actionRouteLoggedIn(context: MainContext) { - if (router.currentRoute.path === '/login' || router.currentRoute.path === '/') { - router.push('/main'); - } - }, - async removeNotification(context: MainContext, payload: { notification: AppNotification, timeout: number }) { - return new Promise((resolve, reject) => { - setTimeout(() => { - commitRemoveNotification(context, payload.notification); - resolve(true); - }, payload.timeout); - }); - }, - async passwordRecovery(context: MainContext, payload: { username: string }) { - const loadingNotification = { content: 'Sending password recovery email', showProgress: true }; - try { - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.passwordRecovery(payload.username), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Password recovery email sent', color: 'success' }); - await dispatchLogOut(context); - } catch (error) { - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { color: 'error', content: 'Incorrect username' }); - } - }, - async resetPassword(context: MainContext, payload: { password: string, token: string }) { - const loadingNotification = { content: 'Resetting password', showProgress: true }; - try { - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.resetPassword(payload.password, payload.token), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Password successfully reset', color: 'success' }); - await dispatchLogOut(context); - } catch (error) { - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { color: 'error', content: 'Error resetting password' }); - } - }, -}; - -const { dispatch } = getStoreAccessors(''); - -export const dispatchCheckApiError = dispatch(actions.actionCheckApiError); -export const dispatchCheckLoggedIn = dispatch(actions.actionCheckLoggedIn); -export const dispatchGetUserProfile = dispatch(actions.actionGetUserProfile); -export const dispatchLogIn = dispatch(actions.actionLogIn); -export const dispatchLogOut = dispatch(actions.actionLogOut); -export const dispatchUserLogOut = dispatch(actions.actionUserLogOut); -export const dispatchRemoveLogIn = dispatch(actions.actionRemoveLogIn); -export const dispatchRouteLoggedIn = dispatch(actions.actionRouteLoggedIn); -export const dispatchRouteLogOut = dispatch(actions.actionRouteLogOut); -export const dispatchUpdateUserProfile = dispatch(actions.actionUpdateUserProfile); -export const dispatchRemoveNotification = dispatch(actions.removeNotification); -export const dispatchPasswordRecovery = dispatch(actions.passwordRecovery); -export const dispatchResetPassword = dispatch(actions.resetPassword); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts deleted file mode 100644 index 58f83978fa..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MainState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const getters = { - hasAdminAccess: (state: MainState) => { - return ( - state.userProfile && - state.userProfile.is_superuser && state.userProfile.is_active); - }, - loginError: (state: MainState) => state.logInError, - dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer, - dashboardMiniDrawer: (state: MainState) => state.dashboardMiniDrawer, - userProfile: (state: MainState) => state.userProfile, - token: (state: MainState) => state.token, - isLoggedIn: (state: MainState) => state.isLoggedIn, - firstNotification: (state: MainState) => state.notifications.length > 0 && state.notifications[0], -}; - -const {read} = getStoreAccessors(''); - -export const readDashboardMiniDrawer = read(getters.dashboardMiniDrawer); -export const readDashboardShowDrawer = read(getters.dashboardShowDrawer); -export const readHasAdminAccess = read(getters.hasAdminAccess); -export const readIsLoggedIn = read(getters.isLoggedIn); -export const readLoginError = read(getters.loginError); -export const readToken = read(getters.token); -export const readUserProfile = read(getters.userProfile); -export const readFirstNotification = read(getters.firstNotification); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts deleted file mode 100644 index 56ba1a0c2f..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { mutations } from './mutations'; -import { getters } from './getters'; -import { actions } from './actions'; -import { MainState } from './state'; - -const defaultState: MainState = { - isLoggedIn: null, - token: '', - logInError: false, - userProfile: null, - dashboardMiniDrawer: false, - dashboardShowDrawer: true, - notifications: [], -}; - -export const mainModule = { - state: defaultState, - mutations, - actions, - getters, -}; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts deleted file mode 100644 index 3e9c8ba2c0..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IUserProfile } from '@/interfaces'; -import { MainState, AppNotification } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - - -export const mutations = { - setToken(state: MainState, payload: string) { - state.token = payload; - }, - setLoggedIn(state: MainState, payload: boolean) { - state.isLoggedIn = payload; - }, - setLogInError(state: MainState, payload: boolean) { - state.logInError = payload; - }, - setUserProfile(state: MainState, payload: IUserProfile) { - state.userProfile = payload; - }, - setDashboardMiniDrawer(state: MainState, payload: boolean) { - state.dashboardMiniDrawer = payload; - }, - setDashboardShowDrawer(state: MainState, payload: boolean) { - state.dashboardShowDrawer = payload; - }, - addNotification(state: MainState, payload: AppNotification) { - state.notifications.push(payload); - }, - removeNotification(state: MainState, payload: AppNotification) { - state.notifications = state.notifications.filter((notification) => notification !== payload); - }, -}; - -const {commit} = getStoreAccessors(''); - -export const commitSetDashboardMiniDrawer = commit(mutations.setDashboardMiniDrawer); -export const commitSetDashboardShowDrawer = commit(mutations.setDashboardShowDrawer); -export const commitSetLoggedIn = commit(mutations.setLoggedIn); -export const commitSetLogInError = commit(mutations.setLogInError); -export const commitSetToken = commit(mutations.setToken); -export const commitSetUserProfile = commit(mutations.setUserProfile); -export const commitAddNotification = commit(mutations.addNotification); -export const commitRemoveNotification = commit(mutations.removeNotification); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts deleted file mode 100644 index be24b63ae9..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IUserProfile } from '@/interfaces'; - -export interface AppNotification { - content: string; - color?: string; - showProgress?: boolean; -} - -export interface MainState { - token: string; - isLoggedIn: boolean | null; - logInError: boolean; - userProfile: IUserProfile | null; - dashboardMiniDrawer: boolean; - dashboardShowDrawer: boolean; - notifications: AppNotification[]; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/modules/admin.ts b/{{cookiecutter.project_slug}}/frontend/src/store/modules/admin.ts new file mode 100644 index 0000000000..8609411522 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/store/modules/admin.ts @@ -0,0 +1,83 @@ +import { api } from "@/api"; +import { VuexModule, Module, Mutation, Action } from "vuex-module-decorators"; +import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from "@/interfaces"; +import { mainStore } from "@/utils/store-accessor"; + +@Module({ name: "admin" }) +export default class AdminModule extends VuexModule { + users: IUserProfile[] = []; + + get adminOneUser() { + return (userId: number) => { + const filteredUsers = this.users.filter((user) => user.id === userId); + if (filteredUsers.length > 0) { + return { ...filteredUsers[0] }; + } + }; + } + + @Mutation + setUsers(payload: IUserProfile[]) { + this.users = payload; + } + + @Mutation + setUser(payload: IUserProfile) { + const users = this.users.filter((user: IUserProfile) => user.id !== payload.id); + users.push(payload); + this.users = users; + } + + @Action + async getUsers() { + try { + const response = await api.getUsers(mainStore.token); + if (response) { + this.setUsers(response.data); + } + } catch (error) { + await mainStore.checkApiError(error); + } + } + + @Action + async updateUser(payload: { id: number; user: IUserProfileUpdate }) { + try { + const loadingNotification = { content: "saving", showProgress: true }; + mainStore.addNotification(loadingNotification); + const response = ( + await Promise.all([ + api.updateUser(mainStore.token, payload.id, payload.user), + await new Promise((resolve, _reject) => setTimeout(() => resolve(), 500)), + ]) + )[0]; + mainStore.setUserProfile(response.data); + mainStore.removeNotification(loadingNotification); + mainStore.addNotification({ + content: "User successfully updated", + color: "success", + }); + } catch (error) { + await mainStore.checkApiError(error); + } + } + + @Action + async createUser(payload: IUserProfileCreate) { + try { + const loadingNotification = { content: "saving", showProgress: true }; + mainStore.addNotification(loadingNotification); + await Promise.all([ + api.createUser(mainStore.token, payload), + await new Promise((resolve, _reject) => setTimeout(() => resolve(), 500)), + ]); + mainStore.removeNotification(loadingNotification); + mainStore.addNotification({ + content: "User successfully created", + color: "success", + }); + } catch (error) { + await mainStore.checkApiError(error); + } + } +} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/modules/main.ts b/{{cookiecutter.project_slug}}/frontend/src/store/modules/main.ts new file mode 100644 index 0000000000..feee5908b5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/store/modules/main.ts @@ -0,0 +1,253 @@ +import { api } from "@/api"; +import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils"; +import router from "@/router"; +import { AxiosError } from "axios"; +import { VuexModule, Module, Mutation, Action } from "vuex-module-decorators"; +import { IUserProfile, IUserProfileUpdate, IAppNotification } from "@/interfaces"; +import { UNAUTHORIZED } from "http-status-codes"; + +@Module({ name: "main" }) +export default class MainModule extends VuexModule { + token = ""; + isLoggedIn: boolean | null = null; + logInError = false; + userProfile: IUserProfile | null = null; + dashboardMiniDrawer = false; + dashboardShowDrawer = true; + notifications: IAppNotification[] = []; + + get hasAdminAccess() { + return ( + this.isLoggedIn && this.userProfile?.is_superuser && this.userProfile?.is_active + ); + } + + get firstNotification() { + return this.notifications.length > 0 && this.notifications[0]; + } + + @Mutation + setToken(payload: string) { + this.token = payload; + } + + @Mutation + setLoggedIn(payload: boolean) { + this.isLoggedIn = payload; + } + + @Mutation + setLogInError(payload: boolean) { + this.logInError = payload; + } + + @Mutation + setUserProfile(payload: IUserProfile | null) { + this.userProfile = payload; + } + + @Mutation + setDashboardMiniDrawer(payload: boolean) { + this.dashboardMiniDrawer = payload; + } + + @Mutation + setDashboardShowDrawer(payload: boolean) { + this.dashboardShowDrawer = payload; + } + + @Mutation + addNotification(payload: IAppNotification) { + this.notifications.push(payload); + } + + @Mutation + removeNotification(payload: IAppNotification) { + this.notifications = this.notifications.filter( + (notification) => notification !== payload, + ); + } + + @Action + async logIn(payload: { username: string; password: string }) { + try { + const response = await api.logInGetToken(payload.username, payload.password); + const token = response.data.access_token; + if (token) { + saveLocalToken(token); + this.setToken(token); + this.setLoggedIn(true); + this.setLogInError(false); + await this.getUserProfile(); + await this.routeLoggedIn(); + this.addNotification({ content: "Logged in", color: "success" }); + } else { + await this.logOut(); + } + } catch (err) { + this.setLogInError(true); + await this.logOut(); + } + } + + @Action + async getUserProfile() { + try { + const response = await api.getMe(this.token); + if (response.data) { + this.setUserProfile(response.data); + } + } catch (error) { + await this.checkApiError(error); + } + } + + @Action + async updateUserProfile(payload: IUserProfileUpdate) { + try { + const loadingNotification = { content: "saving", showProgress: true }; + this.addNotification(loadingNotification); + const response = ( + await Promise.all([ + api.updateMe(this.token, payload), + await new Promise((resolve, _reject) => setTimeout(() => resolve(), 500)), + ]) + )[0]; + this.setUserProfile(response.data); + this.removeNotification(loadingNotification); + this.addNotification({ + content: "Profile successfully updated", + color: "success", + }); + } catch (error) { + await this.checkApiError(error); + } + } + + @Action + async checkLoggedIn() { + if (!this.isLoggedIn) { + let token = this.token; + if (!token) { + const localToken = getLocalToken(); + if (localToken) { + this.setToken(localToken); + token = localToken; + } + } + if (token) { + try { + const response = await api.getMe(token); + this.setLoggedIn(true); + this.setUserProfile(response.data); + } catch (error) { + await this.removeLogIn(); + } + } else { + await this.removeLogIn(); + } + } + } + + @Action + async removeLogIn() { + removeLocalToken(); + this.setToken(""); + this.setLoggedIn(false); + this.setUserProfile(null); + } + + @Action + async logOut() { + await this.removeLogIn(); + await this.routeLogOut(); + } + + @Action + async userLogOut() { + await this.logOut(); + this.addNotification({ content: "Logged out", color: "success" }); + } + + @Action + async routeLogOut() { + if (router.currentRoute.path !== "/login") { + router.push("/login"); + } + } + + @Action + async checkApiError(payload: AxiosError) { + if (payload.response && payload.response.status === UNAUTHORIZED) { + await this.logOut(); + } + } + + @Action + async routeLoggedIn() { + if (router.currentRoute.path === "/login" || router.currentRoute.path === "/") { + router.push("/main/dashboard"); + } + } + + @Action + async removeNotificationDelayed(payload: { + notification: IAppNotification; + timeout: number; + }) { + return new Promise((resolve, _reject) => { + setTimeout(() => { + this.removeNotification(payload.notification); + resolve(true); + }, payload.timeout); + }); + } + + @Action + async recoverPassword(payload: { username: string }) { + const loadingNotification = { + content: "Sending password recovery email", + showProgress: true, + }; + try { + this.addNotification(loadingNotification); + await Promise.all([ + api.passwordRecovery(payload.username), + await new Promise((resolve, _reject) => setTimeout(() => resolve(), 500)), + ]); + this.removeNotification(loadingNotification); + this.addNotification({ + content: "Password recovery email sent", + color: "success", + }); + await this.logOut(); + } catch (error) { + this.removeNotification(loadingNotification); + this.addNotification({ color: "error", content: "Incorrect username" }); + } + } + + @Action + async resetPassword(payload: { password: string; token: string }) { + const loadingNotification = { content: "Resetting password", showProgress: true }; + try { + this.addNotification(loadingNotification); + await Promise.all([ + api.resetPassword(payload.password, payload.token), + await new Promise((resolve, _reject) => setTimeout(() => resolve(), 500)), + ]); + this.removeNotification(loadingNotification); + this.addNotification({ + content: "Password successfully reset", + color: "success", + }); + await this.logOut(); + } catch (error) { + this.removeNotification(loadingNotification); + this.addNotification({ + color: "error", + content: "Error resetting password", + }); + } + } +} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/state.ts deleted file mode 100644 index ecec111cd8..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/state.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MainState } from './main/state'; - -export interface State { - main: MainState; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/utils.ts b/{{cookiecutter.project_slug}}/frontend/src/utils.ts deleted file mode 100644 index ade11b6a2e..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getLocalToken = () => localStorage.getItem('token'); - -export const saveLocalToken = (token: string) => localStorage.setItem('token', token); - -export const removeLocalToken = () => localStorage.removeItem('token'); diff --git a/{{cookiecutter.project_slug}}/frontend/src/utils/index.ts b/{{cookiecutter.project_slug}}/frontend/src/utils/index.ts new file mode 100644 index 0000000000..21d3c56360 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/utils/index.ts @@ -0,0 +1,5 @@ +export const getLocalToken = () => localStorage.getItem("token"); + +export const saveLocalToken = (token: string) => localStorage.setItem("token", token); + +export const removeLocalToken = () => localStorage.removeItem("token"); diff --git a/{{cookiecutter.project_slug}}/frontend/src/utils/store-accessor.ts b/{{cookiecutter.project_slug}}/frontend/src/utils/store-accessor.ts new file mode 100644 index 0000000000..e624f1d8a5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/utils/store-accessor.ts @@ -0,0 +1,20 @@ +import { Store } from "vuex"; +import { getModule } from "vuex-module-decorators"; +import MainModule from "@/store/modules/main"; +import AdminModule from "@/store/modules/admin"; + +let mainStore: MainModule; +let adminStore: AdminModule; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function initializeStores(store: Store): void { + mainStore = getModule(MainModule, store); + adminStore = getModule(AdminModule, store); +} + +export const modules = { + main: MainModule, + admin: AdminModule, +}; + +export { initializeStores, mainStore, adminStore }; diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue b/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue index 28bcb5965e..a21f8fc3de 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue @@ -1,58 +1,74 @@ - + diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue b/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue index bc1a7ade3f..17e926acd4 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue @@ -1,52 +1,92 @@ - + diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue b/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue index 3e680eb171..da362adb95 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue @@ -1,84 +1,132 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue index 421879b1b6..a58ff44063 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue @@ -5,7 +5,7 @@
Dashboard
-
Welcome {{greetedUser}}
+
Welcome {{ greetedUser }}
View Profile @@ -17,21 +17,22 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue index 846d93bd40..5d0967c37d 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue @@ -1,182 +1,190 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue index 71eeaafeff..efa4a277ab 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue @@ -3,36 +3,34 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue index 1282176aaf..278873bd61 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue @@ -1,28 +1,27 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue index 9b35d9a6c1..1470bad8ff 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue @@ -8,76 +8,74 @@ Create User - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue index 892283ec6c..c2bec43c80 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue @@ -1,85 +1,167 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue index 7421233140..985534be24 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue @@ -1,145 +1,192 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue index 25960bd42e..27dd95f16e 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue @@ -7,13 +7,23 @@
Full Name
-
{{userProfile.full_name}}
-
-----
+
+ {{ userProfile.full_name }} +
+
-----
Email
-
{{userProfile.email}}
-
-----
+
+ {{ userProfile.email }} +
+
-----
@@ -25,22 +35,21 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue index dfbea8d874..a0c9355be2 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue @@ -1,97 +1,119 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue index 80e2cc5864..c30168af7a 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue +++ b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue @@ -1,86 +1,121 @@ diff --git a/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts b/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts index b40eed7bea..ae8999c757 100644 --- a/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts +++ b/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts @@ -2,6 +2,11 @@ import { shallowMount } from '@vue/test-utils'; import UploadButton from '@/components/UploadButton.vue'; import '@/plugins/vuetify'; +import Vue from 'vue' +import Vuetify from 'vuetify' + +Vue.use(Vuetify) + describe('UploadButton.vue', () => { it('renders props.title when passed', () => { const title = 'upload a file'; diff --git a/{{cookiecutter.project_slug}}/frontend/tsconfig.json b/{{cookiecutter.project_slug}}/frontend/tsconfig.json index 88cfbc31d8..7c1befda59 100644 --- a/{{cookiecutter.project_slug}}/frontend/tsconfig.json +++ b/{{cookiecutter.project_slug}}/frontend/tsconfig.json @@ -1,32 +1,24 @@ { "compilerOptions": { - "noImplicitAny": false, "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitAny": false, "moduleResolution": "node", "experimentalDecorators": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", - "types": [ - "webpack-env", - "jest" - ], + "types": ["webpack-env", "jest", "vuetify"], "paths": { - "@/*": [ - "src/*" - ] + "@/*": ["src/*"] }, - "lib": [ - "esnext", - "dom", - "dom.iterable", - "scripthost" - ] + "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, "include": [ "src/**/*.ts", @@ -35,7 +27,5 @@ "tests/**/*.ts", "tests/**/*.tsx" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/{{cookiecutter.project_slug}}/frontend/tslint.json b/{{cookiecutter.project_slug}}/frontend/tslint.json deleted file mode 100644 index 2b37e401c3..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/tslint.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "defaultSeverity": "warning", - "extends": [ - "tslint:recommended" - ], - "linterOptions": { - "exclude": [ - "node_modules/**" - ] - }, - "rules": { - "quotemark": [true, "single"], - "indent": [true, "spaces", 2], - "interface-name": false, - "ordered-imports": false, - "object-literal-sort-keys": false, - "no-consecutive-blank-lines": false - } -} diff --git a/{{cookiecutter.project_slug}}/frontend/vue.config.js b/{{cookiecutter.project_slug}}/frontend/vue.config.js index 140713412f..91298a5ff8 100644 --- a/{{cookiecutter.project_slug}}/frontend/vue.config.js +++ b/{{cookiecutter.project_slug}}/frontend/vue.config.js @@ -1,23 +1,5 @@ module.exports = { - // Fix Vuex-typescript in prod: https://github.com/istrib/vuex-typescript/issues/13#issuecomment-409869231 - configureWebpack: (config) => { - if (process.env.NODE_ENV === 'production') { - config.optimization.minimizer[0].options.terserOptions = Object.assign( - {}, - config.optimization.minimizer[0].options.terserOptions, - { - ecma: 5, - compress: { - keep_fnames: true, - }, - warnings: false, - mangle: { - keep_fnames: true, - }, - }, - ); - } - }, + transpileDependencies: ["vuetify"], chainWebpack: config => { config.module .rule('vue')