├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── Docs ├── Admin │ ├── Admin.md │ ├── Authenticated.md │ ├── Composer.md │ └── Unauthenticated.md ├── Layouts │ └── AppLayout.md ├── Resource │ └── Resource.md └── Ui-Components │ ├── DateField.md │ └── Sidebar │ ├── Sidebar.md │ ├── SidebarAction.md │ ├── SidebarHeading.md │ ├── SidebarLink.md │ ├── SidebarNode.md │ ├── SimpleSidebar.md │ └── sidebar.png ├── LICENSE ├── README.md ├── _config.yml ├── babel.config.js ├── cypress.json ├── demo ├── App.vue ├── assets │ ├── camba-icon.png │ └── logo.png ├── components │ ├── AuthCustomView.vue │ ├── CustomSidebar.vue │ ├── UnauthorizedCustomView.vue │ ├── UnauthorizedView.vue │ ├── articles │ │ ├── CreateArticles.vue │ │ ├── EditArticles.vue │ │ ├── ListArticles.vue │ │ └── ShowArticles.vue │ ├── authors │ │ ├── CreateAuthors.vue │ │ ├── EditAuthors.vue │ │ ├── ListAuthors.vue │ │ └── ShowAuthors.vue │ └── magazines │ │ ├── CreateMagazines.vue │ │ ├── EditMagazines.vue │ │ ├── ListMagazines.vue │ │ └── ShowMagazines.vue ├── constants │ └── index.js ├── utils │ └── dates.js └── va-auth-adapter │ └── axios.adapter.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── background.png ├── banner.png ├── camba_icon.png ├── demo.gif ├── favicon.ico ├── index.html └── logo.png ├── src ├── assets │ ├── fonts │ │ └── Montserrat │ │ │ ├── Montserrat-Black.ttf │ │ │ ├── Montserrat-BlackItalic.ttf │ │ │ ├── Montserrat-Bold.ttf │ │ │ ├── Montserrat-BoldItalic.ttf │ │ │ ├── Montserrat-ExtraBold.ttf │ │ │ ├── Montserrat-ExtraBoldItalic.ttf │ │ │ ├── Montserrat-ExtraLight.ttf │ │ │ ├── Montserrat-ExtraLightItalic.ttf │ │ │ ├── Montserrat-Italic.ttf │ │ │ ├── Montserrat-Light.ttf │ │ │ ├── Montserrat-LightItalic.ttf │ │ │ ├── Montserrat-Medium.ttf │ │ │ ├── Montserrat-MediumItalic.ttf │ │ │ ├── Montserrat-Regular.ttf │ │ │ ├── Montserrat-SemiBold.ttf │ │ │ ├── Montserrat-SemiBoldItalic.ttf │ │ │ ├── Montserrat-Thin.ttf │ │ │ ├── Montserrat-ThinItalic.ttf │ │ │ ├── Montserrat.css │ │ │ └── OFL.txt │ └── logo.png ├── components │ ├── Actions │ │ ├── Create │ │ │ ├── Composer.vue │ │ │ ├── Create.vue │ │ │ ├── defaults.js │ │ │ └── index.js │ │ ├── Edit │ │ │ ├── Composer.vue │ │ │ ├── Edit.vue │ │ │ ├── defaults.js │ │ │ └── index.js │ │ ├── List │ │ │ ├── Composer.vue │ │ │ ├── List.vue │ │ │ ├── defaults.js │ │ │ └── index.js │ │ ├── Show │ │ │ ├── Composer.vue │ │ │ ├── Show.vue │ │ │ ├── defaults.js │ │ │ └── index.js │ │ ├── compose.js │ │ └── index.js │ ├── Admin │ │ ├── index.js │ │ └── src │ │ │ ├── Admin.vue │ │ │ ├── Alerts.vue │ │ │ ├── Authenticated.vue │ │ │ ├── Composer.vue │ │ │ ├── Unauthenticated.vue │ │ │ └── defaults.js │ ├── Core │ │ ├── index.js │ │ └── src │ │ │ └── Core.vue │ ├── Layouts │ │ ├── index.js │ │ └── src │ │ │ ├── AppLayout │ │ │ ├── AppLayout.vue │ │ │ └── index.js │ │ │ ├── AuthLayout │ │ │ ├── AuthLayout.vue │ │ │ ├── defaults.js │ │ │ └── index.js │ │ │ ├── HomeLayout │ │ │ ├── HomeLayout.vue │ │ │ └── index.js │ │ │ └── UnauthorizedLayout │ │ │ ├── UnauthorizedLayout.vue │ │ │ └── index.js │ ├── Resource │ │ ├── index.js │ │ └── src │ │ │ ├── Composer.vue │ │ │ ├── Resource.vue │ │ │ └── defaults.js │ └── UiComponents │ │ ├── DateField │ │ ├── index.js │ │ └── src │ │ │ ├── DateField.vue │ │ │ └── defaults.js │ │ ├── DeleteButton │ │ ├── DeleteButton.vue │ │ └── index.js │ │ ├── EditButton │ │ ├── EditButton.vue │ │ └── index.js │ │ ├── Sidebar │ │ ├── Sidebar.vue │ │ ├── SidebarAction.vue │ │ ├── SidebarHeading.vue │ │ ├── SidebarLink.vue │ │ ├── SidebarNode.vue │ │ ├── SimpleSidebar.vue │ │ ├── defaults.js │ │ └── index.js │ │ ├── SimpleText │ │ ├── index.js │ │ └── src │ │ │ └── SimpleText.vue │ │ ├── Spinner │ │ ├── Spinner.vue │ │ └── index.js │ │ ├── TextField │ │ ├── index.js │ │ └── src │ │ │ └── TextField.vue │ │ └── index.js ├── constants │ ├── error.messages.js │ ├── ui.content.default.js │ ├── ui.element.names.js │ └── ui.elements.props.js ├── handlers │ └── error │ │ └── src │ │ └── index.js ├── index.js ├── main.js ├── plugins │ ├── vuetify.js │ └── vuex │ │ ├── index.js │ │ └── subscriptions.js ├── router │ ├── auth.utils.js │ ├── index.js │ ├── route.bindings.js │ └── route.hooks.js ├── store │ ├── modules │ │ ├── alerts.js │ │ ├── crud.js │ │ ├── entities.js │ │ ├── index.js │ │ ├── requests.js │ │ └── resources.js │ └── utils │ │ ├── common.utils.js │ │ ├── create.utils.js │ │ ├── edit.utils.js │ │ ├── list.utils.js │ │ └── show.utils.js ├── styles │ └── main.sass ├── templates │ └── src │ │ ├── en │ │ └── error.json │ │ └── index.js ├── va-auth │ └── src │ │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ └── types │ │ └── index.js └── validators │ └── src │ ├── components │ └── resource.js │ └── index.js ├── tests ├── e2e │ ├── .eslintrc.js │ ├── factory │ │ ├── auth │ │ │ └── index.js │ │ ├── env │ │ │ └── index.js │ │ ├── index.js │ │ ├── resources │ │ │ ├── articles.js │ │ │ ├── authors.js │ │ │ ├── index.js │ │ │ └── magazines.js │ │ ├── store │ │ │ ├── common.utils.js │ │ │ ├── index.js │ │ │ ├── initial.getters.js │ │ │ └── initial.state.js │ │ ├── users │ │ │ └── index.js │ │ └── utils.js │ ├── fixtures │ │ ├── articles.json │ │ ├── authors.json │ │ └── magazines.json │ ├── helpers │ │ └── index.js │ ├── lib │ │ ├── commands.js │ │ ├── helpers.js │ │ └── server.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ ├── articles │ │ │ ├── create.spec.js │ │ │ ├── delete.spec.js │ │ │ ├── edit.spec.js │ │ │ ├── list.spec.js │ │ │ └── show.spec.js │ │ ├── auth-custom │ │ │ └── auth-custom.spec.js │ │ ├── auth │ │ │ └── auth.spec.js │ │ ├── authors │ │ │ ├── create.spec.js │ │ │ ├── edit.spec.js │ │ │ ├── list.spec.js │ │ │ └── show.spec.js │ │ ├── magazines │ │ │ ├── create.spec.js │ │ │ ├── edit.spec.js │ │ │ ├── list.spec.js │ │ │ └── show.spec.js │ │ ├── spinner │ │ │ ├── spinner-create.spec.js │ │ │ ├── spinner-edit.spec.js │ │ │ ├── spinner-list.spec.js │ │ │ └── spinner-show.spec.js │ │ ├── store │ │ │ ├── getters.spec.js │ │ │ └── state.spec.js │ │ ├── ui │ │ │ ├── sidebar.spec.js │ │ │ └── ui.spec.js │ │ └── unauthorized │ │ │ └── unauthorized.spec.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ ├── factory │ ├── admin │ │ └── index.js │ ├── auth │ │ └── index.js │ ├── index.js │ ├── resource │ │ ├── index.js │ │ └── responses.js │ └── store │ │ ├── common.utils.js │ │ ├── index.js │ │ ├── initial.getters.js │ │ ├── initial.mutations.js │ │ ├── initial.state.js │ │ └── modules │ │ └── index.js │ ├── fixtures │ ├── actions │ │ └── index.js │ ├── admin │ │ └── index.js │ ├── auth │ │ └── index.js │ ├── resource │ │ └── magazines.js │ └── ui-components │ │ └── date.input.js │ ├── lib │ ├── constants.js │ └── utils │ │ └── wrapper.js │ └── specs │ ├── components │ ├── actions │ │ ├── create.spec.js │ │ ├── edit.spec.js │ │ ├── list.spec.js │ │ └── show.spec.js │ ├── admin │ │ ├── admin.spec.js │ │ ├── alerts.spec.js │ │ ├── authenticated.spec.js │ │ └── unauthenticated.spec.js │ ├── core.spec.js │ └── resource.spec.js │ ├── layouts │ ├── applayout.spec.js │ ├── auth.spec.js │ ├── homelayout.spec.js │ └── unauthorized.spec.js │ └── ui-components │ ├── date.input.spec.js │ ├── delete.button.spec.js │ ├── edit.button.spec.js │ ├── sidebar.spec.js │ ├── simple.text.spec.js │ ├── spinner.spec.js │ └── text.field.spec.js ├── utils └── server-test │ ├── package-lock.json │ ├── package.json │ ├── server.js │ └── services │ ├── articles.js │ ├── auth.js │ ├── authors.js │ └── magazines.js └── vue.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parser": "vue-eslint-parser", 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "env": { 13 | "node": true 14 | }, 15 | "extends": [ 16 | "plugin:vue/essential", 17 | "eslint:recommended", 18 | "plugin:prettier/recommended" 19 | ], 20 | "parserOptions": { 21 | "parser": "babel-eslint" 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["src/**/*.{js,json,vue}"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | **Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.** 4 | 5 | 13 | 14 | 15 | Fixes #(issue) 16 | 17 | 18 | Closes #(issue) 19 | 20 | ## Type of change 21 | 22 | **Please delete options that are not relevant.** 23 | 24 | 25 | 26 | - [ ] Bug fix (non-breaking change which fixes an issue) 27 | - [ ] New feature (non-breaking change which adds functionality) 28 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 29 | - [ ] This change requires a documentation update 30 | 31 | ## How Has This Been Tested? 32 | 33 | **Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce the cases. Please also list any relevant details for your test configuration** 34 | 35 | 36 | 37 | - [ ] Test A 38 | - [ ] Test B 39 | 40 | 41 | 42 | **Instructions:** 43 | 44 | **1.** First instruction 45 | **2.** Second instruction 46 | **3.** Third instruction 47 | 48 | **Expected result:** a result description 49 | 50 | ## Checklist: 51 | 52 | > The following options in **bold** are required for a PR approval. Please check the boxes only if necessary, it help us minimizing the reviewing process. 53 | 54 | 55 | 56 | - [ ] **I have performed a self-review of my own code** 57 | - [ ] I have commented my code, particularly in hard-to-understand areas 58 | - [ ] I have made corresponding changes to the documentation 59 | - [ ] **My changes generate no new warnings** 60 | - [ ] I have added tests that prove my fix is effective or that my feature works 61 | - [ ] **New and existing unit tests pass locally with my changes** 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # tests 6 | tests/**/coverage 7 | 8 | # e2e tests 9 | tests/e2e/videos 10 | 11 | # local env files 12 | .env.local 13 | .env.*.local 14 | 15 | # Log files 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Editor directories and files 21 | .idea 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw* 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore everything by default 2 | * 3 | 4 | # Keep source 5 | !/src/**/* 6 | /src/main.js 7 | /src/App.vue 8 | 9 | # Keep built files 10 | !/dist/**/* 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.15.3" 4 | addons: 5 | apt: 6 | packages: 7 | # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves 8 | - libgconf-2-4 9 | cache: 10 | # Caches $HOME/.npm when npm ci is default script command 11 | # Caches node_modules in all other cases 12 | npm: true 13 | directories: 14 | # we also need to cache folder with Cypress binary 15 | - ~/.cache 16 | install: 17 | - npm ci 18 | before_script: 19 | - npm i -g vue-cli 20 | script: 21 | - npm run lint 22 | - npm run build 23 | - npm run test:coverage && ./node_modules/.bin/codecov 24 | - npm run test:e2e -- --headless 25 | -------------------------------------------------------------------------------- /Docs/Admin/Authenticated.md: -------------------------------------------------------------------------------- 1 | # `Authenticated` 2 | 3 | The `Authenticated` component is used by the `Admin` component. It can be understood as a wrapper of the main application layout whenever a truthy authentication state is present in the `Admin` component. 4 | 5 | It's responsible of creating the unauthorized routes and rendering a Core component along with other application layouts. 6 | 7 | ## internal props 8 | 9 | ### appLayout 10 | 11 | + **Type:** `Object` 12 | 13 | + **Details:** The `AppLayout` component is assigned by default in the `Admin` component's defaults file. 14 | 15 | > Future feature: Admin should expose appLayout 16 | 17 | ## methods 18 | 19 | ### logout 20 | 21 | + **Details:** implements the `auth/AUTH_LOGOUT_REQUEST` interface from `@va-auth/types` 22 | 23 | ### getUser 24 | 25 | + **Details:** implements the `auth/AUTH_GET_USER` interface from `@va-auth/types` 26 | -------------------------------------------------------------------------------- /Docs/Admin/Composer.md: -------------------------------------------------------------------------------- 1 | # `Admin/Composer` 2 | 3 | The `Composer` wrapper for the `Admin` is nothing more than a functional component that receives an `authProvider` and `options` to render an `Admin` component. By default, the `authProvider` prop will be fed to `@va-auth/store` to obtain a store module that implements `@va-auth/types`. 4 | 5 | ## props 6 | 7 | ### authLayout 8 | 9 | + **Type:** `Object (optional)` 10 | 11 | + **Details:** A component to log in that is passed as prop to `Admin` 12 | 13 | ### authProvider 14 | 15 | + **Type:** `Function` 16 | 17 | + **Details:** a function that implements the `@va-auth/types`. 18 | 19 | + **Example:** [**va-auth-axios-adapter**](https://github.com/Cambalab/va-auth-axios-adapter) 20 | 21 | ### options 22 | 23 | + **Type:** `Object` 24 | 25 | + **Details:** An object with options that are passed as props to `Admin`: 26 | + `authModule`: a vuex store `Object` with properties: `namespaced: true`, state, actions, mutations and getters. 27 | > Corresponds to the `@va-auth` module. 28 | 29 | ### sidebar 30 | 31 | + **Type:** `Object (optional)` 32 | 33 | + **Details:** A sidebar component that is passed as prop to `Admin` 34 | -------------------------------------------------------------------------------- /Docs/Admin/Unauthenticated.md: -------------------------------------------------------------------------------- 1 | # `Unauthenticated` 2 | 3 | The `Unauthenticated` component is used by the `Admin` component. It's just a wrapper for the provided `authLayout` whenever a falsy authentication state is present in the `Admin` component. 4 | 5 | It's responsible of rendering a component that represents the authentication view. 6 | 7 | > The authentication route `/login` is currently defined in the `Admin/defaults`. 8 | 9 | ## props 10 | 11 | ### layout 12 | 13 | + **Type:** `Object` 14 | 15 | + **Details:** A view that is used for user authentication, corresponding to the `/login` route. 16 | 17 | + **Default:** The `Auth` layout. 18 | 19 | ## methods 20 | 21 | ### login 22 | 23 | + **Details:** implements the `auth/AUTH_LOGIN_REQUEST` interface from `@va-auth/types` 24 | -------------------------------------------------------------------------------- /Docs/Layouts/AppLayout.md: -------------------------------------------------------------------------------- 1 | # `AppLayout` 2 | 3 | `AppLayout` is used as default to `appLayout` prop in the `Admin` component. It defines how most of the vue admin visual components are rendered. 4 | 5 | ## props 6 | 7 | ### sidebar 8 | 9 | + **Type:** `Sidebar` 10 | 11 | + **Details:** a `Sidebar` component from the `@va-ui` module. 12 | 13 | + **Default:** [`SimpleSidebar`](/Docs/Ui-Components/Sidebar/SimpleSidebar) 14 | > Note that it's currently assigned by `Admin`. Soon to be moved to `AppLayout` defaults. 15 | 16 | ### title 17 | 18 | + **Type:** `String` 19 | 20 | + **Details:** a text used by the `v-app-bar` as a `v-toolbar-title`. 21 | 22 | + **Default:** `VAppBar` 23 | 24 | ## internal props 25 | 26 | ### va 27 | 28 | + **Type:** `Object` 29 | 30 | + **Details:** An object that contains user related functions that are exposed to layouts: 31 | + `logout`: a function that implements the `auth/AUTH_LOGOUT_REQUEST` interface to call the `authModule`. 32 | + `getUser`: a function that implements the `auth/AUTH_GET_USER` interface to call the `authModule` 33 | 34 | > Interfaces implementation can be found in `@va-auth/types`, while the `authModule` is a `@va-auth/store` module, and it's assigned by the `Admin` component. 35 | 36 | + **Default:** An object with the logout and getUser function that, in turn, use the `@va-auth/types`: `AUTH_LOGOUT_REQUEST`, `AUTH_GET_USER` 37 | 38 | + **Provided by:** [`Authenticated`](/Docs/Admin/Authenticated) 39 | -------------------------------------------------------------------------------- /Docs/Ui-Components/DateField.md: -------------------------------------------------------------------------------- 1 | # DateField 2 | 3 | ### Usage 4 | 5 | In order to Vue Admin understand what kind of input the View should render, we must provide a value to a **type** property to the `input` in the template. 6 | 7 | ```vue 8 | 20 | 53 | ``` 54 | 55 | *It is recommended to implement the parse and/or format functions in a separate module for re-usability* 56 | 57 | ### Supported properties 58 | The DateField component supports the following properties: 59 | * **placeHolder**: The placeholder for the input field (as previously); 60 | * **value**: The value to init the component (as previously); 61 | * **name**: The name of the component (as previously); 62 | * **readonly**: A Boolean indicating if the date in the input field can be modified manually (readonly = false) or only with the datepicker (readonly = true); 63 | * **disabled**: A Boolean indicating if the field is disabled; 64 | * **vDatePickerProps**: An Object with the fields matching the [DatePicker api](https://vuetifyjs.com/en/components/date-pickers#api) and the possible values for them; 65 | * **vMenuProps**: An Object with the fields matching the [Vuetify menu component api](https://vuetifyjs.com/en/components/menus#api) and the possible values for them; 66 | * **format**: A Function that receives a Date formatted string and returns returns a string value to show in the text-field of the DateField component. 67 | * **parse**: A Function that receives a date value and must return a valid string that represents a date for the v-date-picker component 68 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/Sidebar.md: -------------------------------------------------------------------------------- 1 | # `Sidebar` 2 | 3 | A `Sidebar` component that wraps the Sidebar related custom components. 4 | 5 |

6 | Vue Admin Sidebar 7 |

8 |
9 | 10 | Here you can find an example on how to use this component and the available components that you can provide to it as children: 11 | 12 | ```vue 13 | 24 | ... 25 | ``` 26 | 27 | As you've guessed a function `logout` must be defined in order to invoke it in the `SidebarAction` component. 28 | 29 | A more detailed description of each related component can be found in their respective file: 30 | 31 | ## Related components 32 | 33 | - [SidebarHeading](Sidebar/SidebarHeading.md) 34 | - [SidebarAction](Sidebar/SidebarAction.md) 35 | - [SidebarLink](Sidebar/SidebarLink.md) 36 | - [SidebarNode](Sidebar/SidebarNode.md) 37 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/SidebarAction.md: -------------------------------------------------------------------------------- 1 | # `SidebarAction` 2 | 3 | A `SidebarAction` component renders an item in the sidebar that can execute a certain action. 4 | 5 | *It's important to notice that using this component outside the `Sidebar` component is senseless.* 6 | 7 | Here you can find an example on how to use this component: 8 | 9 | ```vue 10 | 19 | 28 | ``` 29 | 30 | ## props 31 | 32 | ### title 33 | 34 | + **Type:** `String` *(optional)* 35 | 36 | + **Description:** A String that will be displayed as a title inside the sidebar item for this action. 37 | 38 | ### action 39 | 40 | + **Type:** `Function` *(optional)* 41 | 42 | + **Description:** A Function that will be executed when the `SidebarAction` item is clicked. 43 | 44 | ### icon 45 | 46 | + **Type:** `String` *(optional*) 47 | 48 | + **Description:** A String representing the name of a [Material Icon](https://cdn.materialdesignicons.com/3.8.95/) to prepend in the `SidebarAction` item. 49 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/SidebarHeading.md: -------------------------------------------------------------------------------- 1 | # `SidebarHeading` 2 | 3 | A `SidebarHeading` component renders a heading item in the sidebar. 4 | 5 | *It's important to notice that not using this component as a `Sidebar` component child is senseless.* 6 | 7 | Here's a usage example: 8 | 9 | ```vue 10 | 24 | ``` 25 | 26 | ## props 27 | 28 | ### avatar 29 | 30 | + **Type:** `Object` 31 | 32 | + **Description:** represents a Vue component that is be rendered at the left side of the sidebar heading. 33 | 34 | + **Default:** A functional component that given `color` and `content` as `avatarProps`, renders a `VListItemAvatar` component. More about [Vuetify avatar components](https://vuetifyjs.com/en/components/avatars). Example: 35 | 36 | > ```javascript 37 | > ... 38 | > avatar: { 39 | > type: Object, 40 | > default: () => ({ 41 | > name: 'SidebarHeadingAvatar', 42 | > functional: true, 43 | > render: function(h, context) { 44 | > const { props } = context 45 | > return ( 46 | > 47 | > {h(props.content)} 48 | > 49 | > ) 50 | > }, 51 | > }), 52 | > }, 53 | > ... 54 | > ``` 55 | 56 | ### avatarProps 57 | 58 | + **Type:** `Object` 59 | 60 | + **Description:** An Object containing props to customize the `avatar` component. 61 | 62 | + **Default:** an object with props to be passed to the `avatar` prop component: `color: String` and `content: Object | VNode`. Example: 63 | 64 | > ```javascript 65 | > ... 66 | > avatarProps: { 67 | > type: Object, 68 | > default: () => ({ 69 | > color: 'teal', 70 | > content: AccountIcon, 71 | > }), 72 | > }, 73 | > ... 74 | > const AccountIcon = { 75 | > name: 'AccountIcon', 76 | > render: function(h) { 77 | > return account_circle 78 | > }, 79 | > } 80 | > ``` 81 | 82 | ### title 83 | 84 | + **Type:** `String` 85 | 86 | + **Description:** A String that will be displayed as title inside the sidebar item. 87 | 88 | + **Default:** `'Menu'` 89 | 90 | ### subTitle 91 | 92 | + **Type:** `String` 93 | 94 | + **Description:** A String that will be displayed as title inside the sidebar item. 95 | 96 | + **Default:** `''` 97 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/SidebarLink.md: -------------------------------------------------------------------------------- 1 | # `SidebarLink` 2 | 3 | A `SidebarLink` component renders an item in the sidebar that can be linked to an specific path in the application. Unlike the `SidebarHeading` component, this component should provide a path relative to this application for one of the declared `Resources`. 4 | 5 | *It's important to notice that using this component outside the `` component is senseless.* 6 | 7 | Here you can find an example on how to use this component: 8 | 9 | ```vue 10 | 19 | ``` 20 | 21 | ## props 22 | 23 | ### title 24 | 25 | + **Type:** `String` *(optional)* 26 | 27 | + **Description:** A String that will be displayed as a title inside the sidebar item. 28 | 29 | ### path 30 | 31 | + **Type:** `String` *(optional)* 32 | 33 | + **Description:** A String representing a relative path. 34 | 35 | ### icon 36 | 37 | + **Type:** `String` *(optional)* 38 | 39 | + **Description:** A String representing the name of the [Material Icon](https://cdn.materialdesignicons.com/3.8.95/) to prepend in the Action item when the group is being displayed. 40 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/SidebarNode.md: -------------------------------------------------------------------------------- 1 | # `SidebarNode` 2 | 3 | A `SidebarNode` component renders an item in the sidebar representing a group parent that on clicked event will toggle the visibility of its children items. 4 | 5 | *It's important to notice that using this component outside the `` component is senseless.* 6 | 7 | Here you can find an example on how to use this component: 8 | 9 | ```vue 10 | 21 | ``` 22 | 23 | ## props 24 | 25 | ### title 26 | 27 | + **Type:** `String` *(optional)* 28 | 29 | + **Description:** A `String` that will be displayed as title inside the sidebar item. 30 | 31 | ### icon 32 | 33 | + **Type:** `String` *(optional)* 34 | 35 | + **Description:** A `String` representing the name of the [Material Icon](https://cdn.materialdesignicons.com/3.8.95/) to prepend in the Action item when the group is being displayed. 36 | 37 | ### iconAlt 38 | 39 | + **Type:** `String` *(optional)* 40 | 41 | + **Description:** A `String` representing the name of the [Material Icon](https://cdn.materialdesignicons.com/3.8.95/) to prepend in the Action item when the group is being displayed. 42 | -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/SimpleSidebar.md: -------------------------------------------------------------------------------- 1 | # `SimpleSidebar` 2 | 3 | This is a custom sidebar created using `Sidebar`, `SidebarHeading`,`SidebarAction`,`SidebarLink`,`SidebarNode` that can be exported from `vue-admin-js`. It's a little less customizable than building your own sidebar but it accepts a few useful props. 4 | 5 | ## internal props 6 | 7 | ### subscriptions 8 | 9 | + **Type:** `Array: Function` 10 | 11 | + **Details:** A list of functions. These are currently used to subscribe events to the store and eventually add items to the menu sidebar. These functions must take an `action: Function` that, in turn, take a `mutation: Object` and a `state: Object` (similar to vuex subsciptions). For example: 12 | > ```javascript 13 | > [ 14 | > action => (mutation, state) => { 15 | > if (mutation.type === 'something') { 16 | > state.someProp = action() 17 | > } 18 | > }, 19 | > ] 20 | > ``` 21 | 22 | + **Default:** A list of one function that adds a route to the store state every time a mutation is triggered: 23 | 24 | > ```javascript 25 | > [ 26 | > action => (mutation, state) => { 27 | > const { namespace, RESOURCES_ADD_ROUTE } = ResourcesTypes 28 | > if (mutation.type === `${namespace}/${RESOURCES_ADD_ROUTE}`) { 29 | > const currentRoutes = state.resources.routes.map(route => { 30 | > return { icon: 'list', title: route.name, link: route.path } 31 | > }) 32 | > action(currentRoutes) 33 | > } 34 | > } 35 | > ] 36 | > ``` 37 | 38 | ### va 39 | 40 | + **Type:** `Object` 41 | 42 | + **Details:** An object that contains user related functions that are exposed to layouts: 43 | + `logout`: a function that implements the `auth/AUTH_LOGOUT_REQUEST` interface to call the `authModule`. 44 | + `getUser`: a function that implements the `auth/AUTH_GET_USER` interface to call the `authModule` 45 | 46 | > Interfaces implementation can be found in `@va-auth/types`, while the `authModule` is a `@va-auth/store` module, and it's assigned by the `Admin` component. 47 | 48 | + **Default:** An object with the logout and getUser function that, in turn, use the `@va-auth/types`: `AUTH_LOGOUT_REQUEST`, `AUTH_GET_USER` 49 | > The va object is designed in the `Authenticated` component. 50 | > The `Authenticated` component defines the va object. 51 | 52 | + **Provided by:** [`AppLayout`](/Docs/Layouts/AppLayout) -------------------------------------------------------------------------------- /Docs/Ui-Components/Sidebar/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/Docs/Ui-Components/Sidebar/sidebar.png -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | [ 5 | '@vue/app', 6 | { 7 | useBuiltIns: 'entry', 8 | }, 9 | ], 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8081", 3 | "pluginsFile": "tests/e2e/plugins/index.js", 4 | "projectId": "qawufu" 5 | } 6 | -------------------------------------------------------------------------------- /demo/assets/camba-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/demo/assets/camba-icon.png -------------------------------------------------------------------------------- /demo/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/demo/assets/logo.png -------------------------------------------------------------------------------- /demo/components/CustomSidebar.vue: -------------------------------------------------------------------------------- 1 | 20 | 67 | -------------------------------------------------------------------------------- /demo/components/UnauthorizedCustomView.vue: -------------------------------------------------------------------------------- 1 | 28 | 49 | 54 | -------------------------------------------------------------------------------- /demo/components/UnauthorizedView.vue: -------------------------------------------------------------------------------- 1 | 14 | 31 | 36 | -------------------------------------------------------------------------------- /demo/components/articles/CreateArticles.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /demo/components/articles/EditArticles.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /demo/components/articles/ListArticles.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | -------------------------------------------------------------------------------- /demo/components/articles/ShowArticles.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /demo/components/authors/CreateAuthors.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /demo/components/authors/EditAuthors.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /demo/components/authors/ListAuthors.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /demo/components/authors/ShowAuthors.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /demo/components/magazines/ShowMagazines.vue: -------------------------------------------------------------------------------- 1 | 41 | 90 | -------------------------------------------------------------------------------- /demo/constants/index.js: -------------------------------------------------------------------------------- 1 | export const rowsPerPage = 10 2 | -------------------------------------------------------------------------------- /demo/utils/dates.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | function parseDate(date) { 4 | return new Date(date).toISOString(true) 5 | } 6 | 7 | function formatDate(date) { 8 | const momentDate = moment(date) 9 | const day = momentDate.date() 10 | const month = momentDate.month() + 1 11 | const year = momentDate.year() 12 | return `${day}/${month}/${year}` 13 | } 14 | 15 | export default { 16 | parseDate, 17 | formatDate, 18 | } 19 | -------------------------------------------------------------------------------- /demo/va-auth-adapter/axios.adapter.js: -------------------------------------------------------------------------------- 1 | import AuthTypes from '@va-auth/types' 2 | 3 | export default (client, options = {}) => { 4 | return (type, params) => { 5 | const { 6 | AUTH_LOGIN_REQUEST, 7 | AUTH_LOGOUT_REQUEST, 8 | AUTH_CHECK_REQUEST, 9 | } = AuthTypes 10 | 11 | const { authFields, authUrl, storageKey, userField } = Object.assign( 12 | { 13 | authFields: { username: 'username', password: 'password' }, 14 | storageKey: 'token', 15 | userField: 'user', 16 | }, 17 | options 18 | ) 19 | 20 | switch (type) { 21 | case AUTH_LOGIN_REQUEST: 22 | return new Promise((resolve, reject) => { 23 | const headers = { 24 | 'Content-Type': 'application/x-www-form-urlencoded', 25 | [authFields.username]: params.username, 26 | [authFields.password]: params.password, 27 | } 28 | const method = 'post' 29 | const url = authUrl 30 | 31 | client({ url, headers, method }) 32 | .then(response => { 33 | const { data } = response 34 | const { [storageKey]: token, [userField]: user } = data 35 | // something more secure maybe? 36 | localStorage.setItem(storageKey, token) 37 | client.defaults.headers.common['Authorization'] = token 38 | resolve(user) 39 | }) 40 | .catch(error => { 41 | localStorage.removeItem(storageKey) 42 | reject(error) 43 | }) 44 | }) 45 | 46 | case AUTH_LOGOUT_REQUEST: 47 | return new Promise(resolve => { 48 | localStorage.removeItem(storageKey) 49 | delete client.defaults.headers.common['Authorization'] 50 | resolve() 51 | }) 52 | 53 | case AUTH_CHECK_REQUEST: 54 | return new Promise((resolve, reject) => { 55 | const token = localStorage.getItem(storageKey) 56 | if (token) { 57 | const url = authUrl 58 | const headers = { 59 | 'Content-Type': 'application/x-www-form-urlencoded', 60 | token, 61 | } 62 | const method = 'get' 63 | client({ url, headers, method }) 64 | .then(response => { 65 | const { data } = response 66 | const { [userField]: user } = data 67 | resolve(user) 68 | }) 69 | .catch(error => { 70 | reject(error) 71 | }) 72 | } else { 73 | reject('Authentication failed.') 74 | } 75 | }) 76 | default: 77 | return Promise.reject(`Unsupported @va-auth action type: ${type}`) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest', 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1', 11 | '@assets(.*)$': '/src/assets/$1', 12 | '@components(.*)$': '/src/components/$1', 13 | '@constants(.*)$': '/src/constants/$1', 14 | '@demo(.*)$': '/demo/$1', 15 | '@handlers(.*)$': '/src/handlers/$1', 16 | '@plugins(.*)$': '/src/plugins/$1', 17 | '@router(.*)$': '/src/router/$1', 18 | '@store(.*)$': '/src/store/$1', 19 | '@templates(.*)$': '/src/templates/src/$1', 20 | '@unit(.*)$': '/tests/unit/$1', 21 | '@va-auth(.*)$': '/src/va-auth/src/$1', 22 | '@validators(.*)$': '/src/validators/src/$1', 23 | }, 24 | snapshotSerializers: ['jest-serializer-vue'], 25 | testMatch: [ 26 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', 27 | ], 28 | collectCoverageFrom: ['**/src/**/*.(js|jsx|ts|tsx|vue)'], 29 | coverageDirectory: 'tests/unit/coverage', 30 | coverageReporters: [ 31 | 'text', 32 | 'text-summary', 33 | 'html', 34 | 'json', 35 | 'json-summary', 36 | 'lcov', 37 | ], 38 | transformIgnorePatterns: ['node_modules/(?!(vuetify)/)'], 39 | testURL: 'http://localhost/', 40 | } 41 | -------------------------------------------------------------------------------- /public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/background.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/banner.png -------------------------------------------------------------------------------- /public/camba_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/camba_icon.png -------------------------------------------------------------------------------- /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/demo.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-admin 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/public/logo.png -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Black.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-Thin.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cambalab/vue-admin/dbc50d1bac6af27338a107f6111a40c557716a38/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Actions/Create/Composer.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/Actions/Create/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defaults - Default attributes for the Create view 3 | * 4 | * @return {Object} An object containing props and methods 5 | */ 6 | export default () => { 7 | /** 8 | * Create View default validations 9 | */ 10 | const composer = { 11 | parentPropKeys: ['resourceName', 'redirect', 'va'], 12 | componentPropKeys: ['title'], 13 | childrenAdapter: { 14 | placeHolder: 'placeHolder', 15 | source: 'label', 16 | type: 'type', 17 | }, 18 | } 19 | 20 | return { 21 | composer, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Actions/Create/index.js: -------------------------------------------------------------------------------- 1 | import Create from './Composer' 2 | 3 | Create.install = function(Vue) { 4 | Vue.component(Create.name, Create) 5 | } 6 | 7 | export default Create 8 | -------------------------------------------------------------------------------- /src/components/Actions/Edit/Composer.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/Actions/Edit/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defaults - Default attributes for the Edit view 3 | * 4 | * @return {Object} An object containing props and methods 5 | */ 6 | export default () => { 7 | /** 8 | * Edit View default composer options 9 | */ 10 | const composer = { 11 | parentPropKeys: ['resourceName', 'va'], 12 | componentPropKeys: ['title'], 13 | childrenAdapter: { 14 | placeHolder: 'placeHolder', 15 | source: 'label', 16 | type: 'type', 17 | }, 18 | } 19 | 20 | return { 21 | composer, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Actions/Edit/index.js: -------------------------------------------------------------------------------- 1 | import Edit from './Composer' 2 | 3 | Edit.install = function(Vue) { 4 | Vue.component(Edit.name, Edit) 5 | } 6 | 7 | export default Edit 8 | -------------------------------------------------------------------------------- /src/components/Actions/List/Composer.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/Actions/List/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defaults - Default attributes for the List view 3 | * 4 | * @return {Object} An object containing props and methods 5 | */ 6 | export default () => { 7 | /** 8 | * List View default composer options 9 | */ 10 | const composer = { 11 | parentPropKeys: [ 12 | 'resourceName', 13 | 'resourceIdName', 14 | 'hasCreate', 15 | 'hasShow', 16 | 'hasEdit', 17 | 'title', 18 | 'va', 19 | ], 20 | componentPropKeys: ['title'], 21 | childrenAdapter: { 22 | alignContent: 'align', 23 | alignHeader: 'alignHeader', 24 | headerText: 'headerText', 25 | sortable: 'sortable', 26 | source: 'label', 27 | }, 28 | } 29 | 30 | return { 31 | composer, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Actions/List/index.js: -------------------------------------------------------------------------------- 1 | import List from './Composer' 2 | 3 | List.install = function(Vue) { 4 | Vue.component(List.name, List) 5 | } 6 | 7 | export default List 8 | -------------------------------------------------------------------------------- /src/components/Actions/Show/Composer.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/Actions/Show/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defaults - Default attributes for the Show view 3 | * 4 | * @return {Object} An object containing props and methods 5 | */ 6 | export default () => { 7 | /** 8 | * Show View default composer options 9 | */ 10 | const composer = { 11 | parentPropKeys: ['resourceIdName', 'resourceName', 'redirect', 'va'], 12 | componentPropKeys: ['title'], 13 | childrenAdapter: { placeHolder: 'placeHolder', source: 'label' }, 14 | } 15 | 16 | return { 17 | composer, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Actions/Show/index.js: -------------------------------------------------------------------------------- 1 | import Show from './Composer' 2 | 3 | Show.install = function(Vue) { 4 | Vue.component(Show.name, Show) 5 | } 6 | 7 | export default Show 8 | -------------------------------------------------------------------------------- /src/components/Actions/index.js: -------------------------------------------------------------------------------- 1 | import Create from './Create' 2 | import Edit from './Edit' 3 | import List from './List' 4 | import Show from './Show' 5 | 6 | export { Create, Edit, List, Show } 7 | -------------------------------------------------------------------------------- /src/components/Admin/index.js: -------------------------------------------------------------------------------- 1 | import Admin from './src/Composer' 2 | 3 | Admin.install = function(Vue) { 4 | Vue.component(Admin.name, Admin) 5 | } 6 | 7 | export default Admin 8 | -------------------------------------------------------------------------------- /src/components/Admin/src/Alerts.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 47 | -------------------------------------------------------------------------------- /src/components/Admin/src/Authenticated.vue: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /src/components/Admin/src/Composer.vue: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /src/components/Admin/src/Unauthenticated.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/components/Admin/src/defaults.js: -------------------------------------------------------------------------------- 1 | import UI_CONTENT from '@constants/ui.content.default' 2 | import { 3 | AppLayout, 4 | AuthLayout, 5 | HomeLayout, 6 | UnauthorizedLayout, 7 | } from '@components/Layouts' 8 | import { SimpleSidebar } from '@components/UiComponents' 9 | import alertsModule from '@store/modules/alerts' 10 | import entitiesModule from '@store/modules/entities' 11 | import requestsModule from '@store/modules/requests' 12 | import resourceModule from '@store/modules/resources' 13 | 14 | /** 15 | * Defaults - Default attributes for the Admin component 16 | * 17 | * @return {Object} An object containing default attributes 18 | */ 19 | export default () => { 20 | const appLayout = AppLayout 21 | const authLayout = AuthLayout 22 | const homeLayout = HomeLayout 23 | const sidebar = SimpleSidebar 24 | const title = UI_CONTENT.MAIN_TOOLBAR_TITLE 25 | const unauthorized = UnauthorizedLayout 26 | 27 | const createUnauthenticatedRoutes = anAuthLayout => [ 28 | { 29 | path: '/login', 30 | name: 'login', 31 | component: anAuthLayout || authLayout, 32 | props: {}, 33 | }, 34 | ] 35 | 36 | const createUnauthorizedRoutes = anUnauthorizedLayout => { 37 | return [ 38 | { 39 | path: '/unauthorized', 40 | name: 'unauthorized', 41 | component: anUnauthorizedLayout || unauthorized, 42 | }, 43 | ] 44 | } 45 | 46 | const createSiteRoutes = ({ homeLayout: aHomeLayout }) => [ 47 | { 48 | path: '/', 49 | name: 'home', 50 | component: aHomeLayout || homeLayout, 51 | props: {}, 52 | }, 53 | ] 54 | 55 | return { 56 | props: { 57 | appLayout, 58 | authLayout, 59 | homeLayout, 60 | sidebar, 61 | title, 62 | unauthorized, 63 | }, 64 | args: { 65 | alertsModule, 66 | createSiteRoutes, 67 | createUnauthenticatedRoutes, 68 | entitiesModule, 69 | requestsModule, 70 | resourceModule, 71 | createUnauthorizedRoutes, 72 | }, 73 | } 74 | } 75 | 76 | /** 77 | * Defaults - Default attributes for the Authenticated component 78 | * 79 | * @return {Object} An object containing default attributes 80 | */ 81 | 82 | export const authenticatedDefaults = { 83 | args: {}, 84 | } 85 | 86 | /** 87 | * Defaults - Default attributes for the Unauthenticated component 88 | * 89 | * @return {Object} An object containing default attributes 90 | */ 91 | 92 | export const unauthenticatedDefaults = { 93 | args: {}, 94 | } 95 | -------------------------------------------------------------------------------- /src/components/Core/index.js: -------------------------------------------------------------------------------- 1 | import Core from './src/Core' 2 | 3 | Core.install = function(Vue) { 4 | Vue.component(Core.name, Core) 5 | } 6 | 7 | export default Core 8 | -------------------------------------------------------------------------------- /src/components/Core/src/Core.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/Layouts/index.js: -------------------------------------------------------------------------------- 1 | import AppLayout from './src/AppLayout' 2 | import AuthLayout from './src/AuthLayout' 3 | import HomeLayout from './src/HomeLayout' 4 | import UnauthorizedLayout from './src/UnauthorizedLayout' 5 | 6 | export { AppLayout, AuthLayout, HomeLayout, UnauthorizedLayout } 7 | -------------------------------------------------------------------------------- /src/components/Layouts/src/AppLayout/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 54 | 55 | 56 | 75 | -------------------------------------------------------------------------------- /src/components/Layouts/src/AppLayout/index.js: -------------------------------------------------------------------------------- 1 | import AppLayout from './AppLayout' 2 | 3 | AppLayout.install = function(Vue) { 4 | Vue.component(AppLayout.name, AppLayout) 5 | } 6 | 7 | export default AppLayout 8 | -------------------------------------------------------------------------------- /src/components/Layouts/src/AuthLayout/defaults.js: -------------------------------------------------------------------------------- 1 | import logo from '@assets/logo.png' 2 | import UI_CONTENT from '@constants/ui.content.default' 3 | 4 | /** 5 | * Defaults - Default attributes for the Auth view 6 | * 7 | * @return {Object} An object containing props and methods 8 | */ 9 | export default () => { 10 | const authFormTitle = AuthFormTitle 11 | const authFooter = AuthFooter 12 | const authMainContent = AuthContent 13 | 14 | const usernameRules = [ 15 | v => !!v || UI_CONTENT.AUTH_ALERT_EMAIL_REQUIRED, 16 | v => 17 | /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(v) || 18 | UI_CONTENT.AUTH_ALERT_INVALID_EMAIL, 19 | ] 20 | const passwordRules = [v => !!v || UI_CONTENT.AUTH_ALERT_PASSWORD_REQUIRED] 21 | 22 | return { 23 | props: { 24 | authFormTitle, 25 | authFooter, 26 | authMainContent, 27 | usernameRules, 28 | passwordRules, 29 | }, 30 | } 31 | } 32 | 33 | const AuthFormTitle = { 34 | name: 'AuthFormTitle', 35 | // eslint-disable-next-line 36 | render: function(h) { 37 | return ( 38 |
39 |

{UI_CONTENT.AUTH_CONTAINER_TITLE}

40 |
41 | ) 42 | }, 43 | } 44 | 45 | const AuthContent = { 46 | name: 'AuthContent', 47 | // eslint-disable-next-line 48 | render: function(h) { 49 | return ( 50 |
51 | 52 |

Welcome to Vue-Admin

53 |
54 | ) 55 | }, 56 | } 57 | 58 | const AuthFooter = { 59 | name: 'AuthFooter', 60 | // eslint-disable-next-line 61 | render: function(h) { 62 | return null 63 | }, 64 | } 65 | 66 | const styles = { 67 | authFormContainer: { 68 | margin: '10px', 69 | }, 70 | authFormTitle: { 71 | fontWeight: '300', 72 | }, 73 | vaImg: { 74 | margin: 'auto', 75 | width: '140px', 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Layouts/src/AuthLayout/index.js: -------------------------------------------------------------------------------- 1 | import AuthLayout from './AuthLayout' 2 | 3 | AuthLayout.install = function(Vue) { 4 | Vue.component(AuthLayout.name, AuthLayout) 5 | } 6 | 7 | export default AuthLayout 8 | -------------------------------------------------------------------------------- /src/components/Layouts/src/HomeLayout/HomeLayout.vue: -------------------------------------------------------------------------------- 1 | 13 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /src/components/Layouts/src/HomeLayout/index.js: -------------------------------------------------------------------------------- 1 | import HomeLayout from './HomeLayout' 2 | 3 | HomeLayout.install = function(Vue) { 4 | Vue.component(HomeLayout.name, HomeLayout) 5 | } 6 | 7 | export default HomeLayout 8 | -------------------------------------------------------------------------------- /src/components/Layouts/src/UnauthorizedLayout/UnauthorizedLayout.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 63 | -------------------------------------------------------------------------------- /src/components/Layouts/src/UnauthorizedLayout/index.js: -------------------------------------------------------------------------------- 1 | import UnauthorizedLayout from './UnauthorizedLayout' 2 | 3 | UnauthorizedLayout.install = function(Vue) { 4 | Vue.component(UnauthorizedLayout.name, UnauthorizedLayout) 5 | } 6 | 7 | export default UnauthorizedLayout 8 | -------------------------------------------------------------------------------- /src/components/Resource/index.js: -------------------------------------------------------------------------------- 1 | import Resource from './src/Composer' 2 | 3 | Resource.install = function(Vue) { 4 | Vue.component(Resource.name, Resource) 5 | } 6 | 7 | export default Resource 8 | -------------------------------------------------------------------------------- /src/components/Resource/src/Composer.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /src/components/UiComponents/DateField/index.js: -------------------------------------------------------------------------------- 1 | import DateField from './src/DateField.vue' 2 | 3 | DateField.install = function(Vue) { 4 | Vue.component(DateField.name, DateField) 5 | } 6 | 7 | export default DateField 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/DateField/src/DateField.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 103 | -------------------------------------------------------------------------------- /src/components/UiComponents/DateField/src/defaults.js: -------------------------------------------------------------------------------- 1 | import { handleEmptyProp } from '@handlers/error/src' 2 | 3 | /** 4 | * Defaults - Default attributes for the DateField component 5 | * 6 | * @return {Object} An object containing props and methods 7 | */ 8 | export default () => { 9 | const component = 'DateField' 10 | 11 | function _vDatePickerProps() { 12 | return { noTitle: true } 13 | } 14 | 15 | function _vMenuProps() { 16 | return {} 17 | } 18 | 19 | /** 20 | * DateField default props 21 | */ 22 | const disabled = false 23 | const format = handleEmptyProp({ prop: 'format', at: component }) 24 | const name = 'va-date-input' 25 | const parse = handleEmptyProp({ prop: 'parse', at: component }) 26 | const readonly = true 27 | const vDatePickerProps = _vDatePickerProps 28 | const vMenuProps = _vMenuProps 29 | 30 | return { 31 | props: { 32 | disabled, 33 | format, 34 | name, 35 | parse, 36 | readonly, 37 | vDatePickerProps, 38 | vMenuProps, 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/UiComponents/DeleteButton/DeleteButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 87 | -------------------------------------------------------------------------------- /src/components/UiComponents/DeleteButton/index.js: -------------------------------------------------------------------------------- 1 | import DeleteButton from './DeleteButton' 2 | 3 | DeleteButton.install = function(Vue) { 4 | Vue.component(DeleteButton.name, DeleteButton) 5 | } 6 | 7 | export default DeleteButton 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/EditButton/EditButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 79 | -------------------------------------------------------------------------------- /src/components/UiComponents/EditButton/index.js: -------------------------------------------------------------------------------- 1 | import EditButton from './EditButton' 2 | 3 | EditButton.install = function(Vue) { 4 | Vue.component(EditButton.name, EditButton) 5 | } 6 | 7 | export default EditButton 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/SidebarAction.vue: -------------------------------------------------------------------------------- 1 | 13 | 23 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/SidebarHeading.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 40 | 41 | 50 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/SidebarLink.vue: -------------------------------------------------------------------------------- 1 | 13 | 34 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/SidebarNode.vue: -------------------------------------------------------------------------------- 1 | 20 | 36 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/SimpleSidebar.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 95 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/defaults.js: -------------------------------------------------------------------------------- 1 | import { VListItemAvatar, VAvatar, VIcon } from 'vuetify/lib' 2 | import { Types as ResourcesTypes } from '@store/modules/resources' 3 | 4 | /** 5 | * Defaults - Default attributes for the SimpleSidebar view 6 | * 7 | * @return {Object} An object containing props and methods 8 | */ 9 | export default () => { 10 | const menuItems = [ 11 | { 12 | icon: 'keyboard_arrow_up', 13 | 'icon-alt': 'keyboard_arrow_down', 14 | title: 'Resources', 15 | children: [], 16 | model: {}, 17 | value: true, 18 | }, 19 | ] 20 | 21 | const subscriptions = [ 22 | action => (mutation, state) => { 23 | const { namespace, RESOURCES_ADD_ROUTE } = ResourcesTypes 24 | if (mutation.type === `${namespace}/${RESOURCES_ADD_ROUTE}`) { 25 | const currentRoutes = state.resources.routes.map(route => { 26 | return { icon: 'list', title: route.name, link: route.path } 27 | }) 28 | action(currentRoutes) 29 | } 30 | }, 31 | ] 32 | 33 | return { 34 | data: { 35 | menuItems, 36 | }, 37 | props: { 38 | subscriptions, 39 | }, 40 | } 41 | } 42 | 43 | /** 44 | * Defaults - Default attributes for the SidebarHeading view 45 | * 46 | * @return {Object} An object containing props and methods 47 | */ 48 | export const sidebarHeadingDefaults = () => { 49 | const avatar = Avatar 50 | const avatarProps = { 51 | color: 'teal', 52 | content: AccountIcon, 53 | } 54 | const title = 'Menu' 55 | const subTitle = '' 56 | 57 | return { 58 | props: { 59 | avatar, 60 | avatarProps, 61 | title, 62 | subTitle, 63 | }, 64 | } 65 | } 66 | 67 | const AccountIcon = { 68 | name: 'AccountIcon', 69 | // eslint-disable-next-line 70 | render: function(h) { 71 | return account_circle 72 | }, 73 | } 74 | 75 | const Avatar = { 76 | name: 'SidebarHeadingAvatar', 77 | functional: true, 78 | // eslint-disable-next-line 79 | render: function(h, context) { 80 | const { props } = context 81 | return ( 82 | 83 | {h(props.content)} 84 | 85 | ) 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /src/components/UiComponents/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import Sidebar from './Sidebar' 2 | import SidebarAction from './SidebarAction' 3 | import SidebarHeading from './SidebarHeading' 4 | import SidebarLink from './SidebarLink' 5 | import SidebarNode from './SidebarNode' 6 | 7 | export { Sidebar, SidebarAction, SidebarHeading, SidebarLink, SidebarNode } 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/SimpleText/index.js: -------------------------------------------------------------------------------- 1 | import SimpleText from './src/SimpleText' 2 | 3 | SimpleText.install = function(Vue) { 4 | Vue.component(SimpleText.name, SimpleText) 5 | } 6 | 7 | export default SimpleText 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/SimpleText/src/SimpleText.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /src/components/UiComponents/Spinner/Spinner.vue: -------------------------------------------------------------------------------- 1 | 13 | 53 | 75 | -------------------------------------------------------------------------------- /src/components/UiComponents/Spinner/index.js: -------------------------------------------------------------------------------- 1 | import Spinner from './Spinner' 2 | 3 | Spinner.install = function(Vue) { 4 | Vue.component(Spinner.name, Spinner) 5 | } 6 | 7 | export default Spinner 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/TextField/index.js: -------------------------------------------------------------------------------- 1 | import TextField from './src/TextField' 2 | 3 | TextField.install = function(Vue) { 4 | Vue.component(TextField.name, TextField) 5 | } 6 | 7 | export default TextField 8 | -------------------------------------------------------------------------------- /src/components/UiComponents/TextField/src/TextField.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 45 | -------------------------------------------------------------------------------- /src/components/UiComponents/index.js: -------------------------------------------------------------------------------- 1 | import DateField from './DateField' 2 | import SimpleSidebar from './Sidebar/SimpleSidebar' 3 | import DeleteButton from './DeleteButton' 4 | import EditButton from './EditButton' 5 | import TextField from './TextField' 6 | import SimpleText from './SimpleText' 7 | import Spinner from './Spinner' 8 | import { 9 | Sidebar, 10 | SidebarLink, 11 | SidebarNode, 12 | SidebarAction, 13 | SidebarHeading, 14 | } from './Sidebar' 15 | 16 | export { 17 | DateField, 18 | SimpleSidebar, 19 | DeleteButton, 20 | EditButton, 21 | TextField, 22 | SimpleText, 23 | Spinner, 24 | Sidebar, 25 | SidebarLink, 26 | SidebarNode, 27 | SidebarAction, 28 | SidebarHeading, 29 | } 30 | -------------------------------------------------------------------------------- /src/constants/error.messages.js: -------------------------------------------------------------------------------- 1 | import templates from '@templates' 2 | 3 | const buildMessage = templates('en.error') 4 | 5 | function parseErrorDetails(details) { 6 | return details.map(detail => `\t${detail.message}`).join('\n') 7 | } 8 | 9 | export default { 10 | UNDEFINED_PROPERTY: { 11 | with: ({ prop, at }) => { 12 | return buildMessage('UNDEFINED_PROPERTY', { prop, at }) 13 | }, 14 | }, 15 | INVALID_SCHEMA: { 16 | with: ({ prop, at, details }) => { 17 | const _details = parseErrorDetails(details) 18 | return buildMessage('INVALID_SCHEMA', { prop, at, details: _details }) 19 | }, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/constants/ui.elements.props.js: -------------------------------------------------------------------------------- 1 | export default { 2 | rowsPerPage: 10, 3 | dateInputMaxWidthNoLandscape: '290px', 4 | dateInputMaxWidthLandscape: '580px', 5 | } 6 | -------------------------------------------------------------------------------- /src/handlers/error/src/index.js: -------------------------------------------------------------------------------- 1 | import ERROR_MESSAGES from '@constants/error.messages' 2 | import { validateSchema } from '@validators' 3 | 4 | /** 5 | * handleEmptyProp - Given a prop, throws an error with the proper user 6 | * feedback 7 | * 8 | * @param {String} prop The property of a component 9 | * @param {String} at The name of the the error is coming from. This param is 10 | * useful add references to specific documentation on error 11 | * messages. 12 | */ 13 | export const handleEmptyProp = ({ prop, at }) => () => { 14 | const { UNDEFINED_PROPERTY } = ERROR_MESSAGES 15 | throw new Error(UNDEFINED_PROPERTY.with({ prop, at })) 16 | } 17 | 18 | /** 19 | * handleSchemaValidation - Given a schema, a prop and a component name, returns 20 | * a validation result or throws an error 21 | * 22 | * @param {Object} schema A property to validate 23 | * @param {String} prop The name of the property 24 | * @param {String} at The name of the component 25 | * 26 | * @return {Object} The given property 27 | */ 28 | export const handleSchemaValidation = ({ schema, prop, at }) => { 29 | const validation = validateSchema(prop, schema) 30 | if (validation.error) { 31 | const { INVALID_SCHEMA } = ERROR_MESSAGES 32 | const { details } = validation.error 33 | throw new Error(INVALID_SCHEMA.with({ prop, at, details })) 34 | } 35 | return validation 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Create, Edit, List, Show } from '@components/Actions' 2 | import { 3 | DeleteButton, 4 | EditButton, 5 | Sidebar, 6 | SidebarLink, 7 | SidebarNode, 8 | SidebarAction, 9 | SidebarHeading, 10 | } from '@components/UiComponents' 11 | import Admin from '@components/Admin' 12 | import AuthTypes from '@va-auth/types' 13 | import Resource from '@components/Resource' 14 | import { Types as AlertTypes } from '@store/modules/alerts' 15 | import { UnauthorizedLayout } from '@components/Layouts' 16 | import { name, description, version } from '../package.json' 17 | 18 | const components = [ 19 | Admin, 20 | Resource, 21 | Create, 22 | DeleteButton, 23 | Edit, 24 | EditButton, 25 | List, 26 | Show, 27 | Sidebar, 28 | SidebarLink, 29 | SidebarNode, 30 | SidebarAction, 31 | SidebarHeading, 32 | ] 33 | 34 | const install = function(Vue) { 35 | components.forEach(component => { 36 | Vue.component(component.name, component) 37 | }) 38 | } 39 | 40 | if (typeof window !== 'undefined' && window.Vue) { 41 | install(window.Vue) 42 | } 43 | 44 | export { 45 | // Package data 46 | name, 47 | description, 48 | version, 49 | // Exports Actions components 50 | Create, 51 | DeleteButton, 52 | Edit, 53 | EditButton, 54 | List, 55 | Show, 56 | // Exports Core components 57 | Admin, 58 | Resource, 59 | // Exports Layouts 60 | UnauthorizedLayout, 61 | // Exports Ui Components 62 | Sidebar, 63 | SidebarLink, 64 | SidebarNode, 65 | SidebarAction, 66 | SidebarHeading, 67 | // Exports Types 68 | AlertTypes, 69 | AuthTypes, 70 | } 71 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | import 'material-design-icons-iconfont/dist/material-design-icons.css' 3 | import './styles/main.sass' 4 | import './assets/fonts/Montserrat/Montserrat.css' 5 | import Vue from 'vue' 6 | import Vuex from 'vuex' 7 | import VueRouter from 'vue-router' 8 | import vuetify from '@plugins/vuetify' 9 | import { subscriptionsPlugin } from '@plugins/vuex' 10 | import createLogger from 'vuex/dist/logger' 11 | 12 | Vue.config.productionTip = false 13 | 14 | Vue.use(VueRouter) 15 | const routes = [{}] 16 | const router = new VueRouter(routes) 17 | 18 | import App from '@demo/App.vue' 19 | 20 | Vue.use(Vuex) 21 | const store = new Vuex.Store({ 22 | plugins: [subscriptionsPlugin, createLogger()], 23 | }) 24 | 25 | const app = new Vue({ 26 | router, 27 | store, 28 | vuetify, 29 | render: h => h(App), 30 | }).$mount('#app') 31 | 32 | if (window.Cypress) { 33 | // exposes the app, only available during E2E tests 34 | window.app = app 35 | } 36 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import '@mdi/font/css/materialdesignicons.css' 4 | import 'vuetify/dist/vuetify.min.css' 5 | 6 | Vue.use(Vuetify) 7 | 8 | export default new Vuetify({ 9 | icons: { 10 | iconfont: 'mdi', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /src/plugins/vuex/index.js: -------------------------------------------------------------------------------- 1 | import { subscriptions } from './subscriptions' 2 | 3 | export const subscriptionsPlugin = store => { 4 | subscriptions.forEach(subscription => store.subscribe(subscription(store))) 5 | } 6 | -------------------------------------------------------------------------------- /src/plugins/vuex/subscriptions.js: -------------------------------------------------------------------------------- 1 | import AuthTypes from '@va-auth/types' 2 | import { Types as AlertTypes } from '@store/modules/alerts' 3 | import UI_CONTENT from '@constants/ui.content.default' 4 | 5 | const { 6 | namespace: authNamespace, 7 | AUTH_LOGIN_FAILURE, 8 | AUTH_LOGIN_SUCCESS, 9 | } = AuthTypes 10 | const { namespace: alertsNamespace, ALERTS_SHOW_SNACKBAR } = AlertTypes 11 | 12 | const loginAlert = store => mutation => { 13 | const mutationSubscription = `${authNamespace}/${AUTH_LOGIN_SUCCESS}` 14 | if (mutation.type === mutationSubscription) { 15 | const mutationCommit = `${alertsNamespace}/${ALERTS_SHOW_SNACKBAR}` 16 | const args = { 17 | color: UI_CONTENT.SNACKBAR_SUCCESS_COLOR, 18 | text: username => 19 | UI_CONTENT.AUTH_SNACKBAR_LOGIN_SUCCESS.with({ username }), 20 | } 21 | const { email: username } = mutation.payload 22 | const text = args.text(username) 23 | store.commit(mutationCommit, { ...args, text }) 24 | } 25 | } 26 | 27 | const failedAuthentication = store => mutation => { 28 | const mutationSubscription = `${authNamespace}/${AUTH_LOGIN_FAILURE}` 29 | if (mutation.type === mutationSubscription) { 30 | const mutationCommit = `${alertsNamespace}/${ALERTS_SHOW_SNACKBAR}` 31 | const args = { 32 | color: UI_CONTENT.SNACKBAR_ERROR_COLOR, 33 | text: UI_CONTENT.AUTH_SNACKBAR_INVALID_USER_PASSWORD, 34 | } 35 | store.commit(mutationCommit, args) 36 | } 37 | } 38 | 39 | export const subscriptions = [failedAuthentication, loginAlert] 40 | -------------------------------------------------------------------------------- /src/router/auth.utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Auth Utils - A function used to create utilities 3 | * 4 | * @param {Object} store The global Vuex store variable 5 | * 6 | * @return {Object} A set of functions to be used for Authentication 7 | */ 8 | export default ({ store, types }) => { 9 | const { namespace, AUTH_IS_AUTHENTICATED, AUTH_GET_USER } = types 10 | return { 11 | /** 12 | * checkAuthentication - Indicates whether the user is authenticated or not. 13 | * 14 | * @return {Boolean} 15 | */ 16 | isAuthenticated() { 17 | return store.getters[`${namespace}/${AUTH_IS_AUTHENTICATED}`] 18 | }, 19 | 20 | /** 21 | * getUser - Returns the user object from the store 22 | * 23 | * @return {Object} The current logged user object 24 | */ 25 | getUser() { 26 | return store.getters[`${namespace}/${AUTH_GET_USER}`] 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | const Router = {} 2 | 3 | Router.redirect = ({ router, resource, view, id }) => { 4 | ;({ 5 | list: () => { 6 | router.push({ name: `${resource}/${view}` }) 7 | }, 8 | show: () => { 9 | router.push({ name: `${resource}/${view}`, params: { id } }) 10 | }, 11 | }[view]()) 12 | } 13 | 14 | export default Router 15 | -------------------------------------------------------------------------------- /src/router/route.hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Route Hooks - A function used to create route hooks 3 | * 4 | * @param {Boolean} isPublic Indicates whether or not a route needs authentication to be visited 5 | * @param {Array} permissions An array of route permissions as Strings 6 | * @param {Object} store A group of store auth actions 7 | * @param {String} userPermissionsField The name of the permissions field in a user object 8 | * @return {type} An object with hook functions 9 | */ 10 | export default ({ isPublic, permissions, store, userPermissionsField }) => { 11 | const requiresAuth = !isPublic 12 | 13 | const beforeEnter = (to, from, next) => { 14 | if (requiresAuth) { 15 | // It's a private route 16 | 17 | const isAuthenticated = store.isAuthenticated() 18 | if (!isAuthenticated) { 19 | // User is not authenticated 20 | next({ 21 | path: '/login', 22 | params: { nextUrl: to.fullPath }, 23 | }) 24 | } else { 25 | // User is authenticated 26 | if (permissions.length > 0) { 27 | // Route has permissions restriction 28 | 29 | const user = store.getUser() 30 | const { [userPermissionsField]: userPermissions } = user 31 | const userHasPermissions = permissions.some(permission => { 32 | return userPermissions.indexOf(permission) > -1 33 | }) 34 | if (userHasPermissions) { 35 | // User is authenticated and has route permissions 36 | next() 37 | } else { 38 | // User is authenticated but does not have route permissions 39 | next('/unauthorized') 40 | } 41 | } else { 42 | // Route has no permissions restriction 43 | next() 44 | } 45 | } 46 | } else { 47 | // It's a public route 48 | next() 49 | } 50 | } 51 | 52 | return { 53 | beforeEnter, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/store/modules/alerts.js: -------------------------------------------------------------------------------- 1 | export const Types = { 2 | namespace: 'alerts', 3 | 4 | ALERTS_HIDE_SNACKBAR: 'ALERTS_HIDE_SNACKBAR', 5 | ALERTS_SHOW_SNACKBAR: 'ALERTS_SHOW_SNACKBAR', 6 | 7 | ALERTS_GET_SNACKBAR_STATUS: 'ALERTS_GET_SNACKBAR_STATUS', 8 | } 9 | 10 | export default { 11 | namespaced: true, 12 | state: { 13 | snackbar: { 14 | color: '', 15 | isVisible: false, 16 | text: '', 17 | }, 18 | }, 19 | mutations: { 20 | [Types.ALERTS_HIDE_SNACKBAR](state) { 21 | state.snackbar.color = '' 22 | state.snackbar.text = '' 23 | state.snackbar.isVisible = false 24 | }, 25 | [Types.ALERTS_SHOW_SNACKBAR](state, { color, text }) { 26 | state.snackbar.color = color 27 | state.snackbar.text = text 28 | state.snackbar.isVisible = true 29 | }, 30 | }, 31 | getters: { 32 | [Types.ALERTS_GET_SNACKBAR_STATUS]: state => state.snackbar, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /src/store/modules/entities.js: -------------------------------------------------------------------------------- 1 | export const Types = { 2 | namespace: 'entities', 3 | 4 | ENTITIES_CREATE_FORM: 'ENTITIES_CREATE_FORM', 5 | ENTITIES_UPDATE_FORM: 'ENTITIES_UPDATE_FORM', 6 | 7 | ENTITIES_GET_ENTITY: 'ENTITIES_GET_ENTITY', 8 | } 9 | 10 | const initForm = (state, { formType, entity }) => { 11 | state[formType] = state[formType] || {} 12 | state[formType][entity] = state[formType][entity] || {} 13 | } 14 | 15 | export default { 16 | namespaced: true, 17 | state: {}, 18 | mutations: { 19 | [`${Types.ENTITIES_CREATE_FORM}`]: initForm, 20 | [`${Types.ENTITIES_UPDATE_FORM}`]( 21 | state, 22 | { formType, entity, resourceKey, value } 23 | ) { 24 | initForm(state, { formType, entity }) 25 | state[formType][entity][resourceKey] = value 26 | }, 27 | }, 28 | getters: { 29 | [`${Types.ENTITIES_GET_ENTITY}`]: state => state, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | import createCrudModule from './crud' 2 | 3 | export { createCrudModule } 4 | -------------------------------------------------------------------------------- /src/store/modules/requests.js: -------------------------------------------------------------------------------- 1 | export const Types = { 2 | namespace: 'requests', 3 | 4 | REQUESTS_SET_LOADING: 'REQUESTS_SET_LOADING', 5 | 6 | REQUESTS_IS_LOADING: 'REQUESTS_IS_LOADING', 7 | } 8 | 9 | export default { 10 | namespaced: true, 11 | state: { 12 | isLoading: false, 13 | }, 14 | mutations: { 15 | [`${Types.REQUESTS_SET_LOADING}`](state, payload) { 16 | state.isLoading = payload.isLoading 17 | }, 18 | }, 19 | getters: { 20 | [`${Types.REQUESTS_IS_LOADING}`]: state => state.isLoading, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/store/modules/resources.js: -------------------------------------------------------------------------------- 1 | export const Types = { 2 | namespace: 'resources', 3 | 4 | RESOURCES_ADD_ROUTE: 'RESOURCES_ADD_ROUTE', 5 | 6 | RESOURCES_GET_ALL_ROUTES: 'RESOURCES_GET_ALL_ROUTES', 7 | } 8 | 9 | export default { 10 | namespaced: true, 11 | state: { 12 | routes: [], 13 | }, 14 | mutations: { 15 | [Types.RESOURCES_ADD_ROUTE]( 16 | { routes }, 17 | { path, name, addedRouteCallback } 18 | ) { 19 | let matchingPathRouteIndex 20 | let newRoute = { path, name } 21 | routes.forEach( 22 | (route, index) => 23 | route.name === name && (matchingPathRouteIndex = index) 24 | ) 25 | if (matchingPathRouteIndex !== undefined) { 26 | routes[matchingPathRouteIndex] = newRoute 27 | } else { 28 | routes.push(newRoute) 29 | addedRouteCallback() 30 | } 31 | }, 32 | }, 33 | getters: { 34 | [Types.RESOURCES_GET_ALL_ROUTES]: state => state.routes, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /src/store/utils/create.utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | submitEntity, 3 | initEntity, 4 | updateEntity, 5 | getEntityForm, 6 | } from './common.utils' 7 | import { Types as CrudTypes } from '@store/modules/crud' 8 | 9 | /** 10 | * Create View Utils - A function used to create utilities 11 | * 12 | * @param {String} redirectView A view the router will redirect to on submit 13 | * @param {String} resourceIdName The name of the id of a resource 14 | * @param {String} resourceName The name of the resource 15 | * @param {Object} store The global Vuex store variable 16 | * @param {Object} router The global Vue router variable 17 | * @param {Object} parseResponses An object containing a parseSingle function 18 | * and a parseList function to be used on submit actions. 19 | * 20 | * @return {Object} A set of functions to be used in a Create form. 21 | */ 22 | export default ({ 23 | redirectView, 24 | resourceIdName, 25 | resourceName, 26 | store, 27 | router, 28 | parseResponses, 29 | }) => { 30 | return { 31 | /** 32 | * getEntityForm - Gets the current 'resourceName' entity. The value does not 33 | * exist until a user inputs data using 'updateEntity'. 34 | * 35 | * @return {Object} a 'resourceName' object with updated data from the form. 36 | */ 37 | getEntityForm() { 38 | const formType = 'createForm' 39 | return getEntityForm({ store, resourceName, formType }) 40 | }, 41 | 42 | /** 43 | * updateEntity - Given a key and a value, updates the 'resourceName' entity 44 | * in the store. 45 | * 46 | * @param {String} resourceKey A 'resourceName' attribute key 47 | * @param {String} value A given value to be stored 48 | */ 49 | updateEntity({ resourceKey, value }) { 50 | const formType = 'createForm' 51 | updateEntity({ 52 | resourceKey, 53 | value, 54 | store, 55 | resourceName, 56 | formType, 57 | }) 58 | }, 59 | 60 | /** 61 | * initEntity - Init the 'resourceName' entity in the store. 62 | */ 63 | initEntity() { 64 | const formType = 'createForm' 65 | initEntity({ 66 | store, 67 | resourceName, 68 | formType, 69 | }) 70 | }, 71 | 72 | /** 73 | * submitEntity - Dispatchs a create request 74 | * 75 | * @return {Promise} A pending promise. 76 | */ 77 | submitEntity() { 78 | const { VUEX_CRUD_PUT } = CrudTypes 79 | const actionType = VUEX_CRUD_PUT 80 | const actionTypeParams = { data: this.getEntityForm() } 81 | submitEntity({ 82 | resourceName, 83 | actionType, 84 | actionTypeParams, 85 | store, 86 | router, 87 | redirectView, 88 | resourceIdName, 89 | parseResponses, 90 | }) 91 | }, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/store/utils/list.utils.js: -------------------------------------------------------------------------------- 1 | import { fetchList, getList } from './common.utils' 2 | 3 | /** 4 | * List View Utils - A function used to create utilities 5 | * 6 | * @param {String} resourceName The name of the resource 7 | * @param {Object} store The global Vuex store variable 8 | * 9 | * @return {Object} A set of functions to be used in an Edit form. 10 | */ 11 | export default ({ resourceName, store }) => { 12 | return { 13 | /** 14 | * fetchList - Fetchs a set of 'resourceName' using the Vuex Crud getters 15 | * 16 | * @return {Array} An array of 'resourceName' elements 17 | */ 18 | fetchList() { 19 | return fetchList({ resourceName, store }) 20 | }, 21 | 22 | /** 23 | * getList - Gets a set of 'resourceName' elements from the store 24 | * 25 | * @return {Array} An array of 'resourceName' elements 26 | */ 27 | getList() { 28 | return getList({ resourceName, store }) 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/store/utils/show.utils.js: -------------------------------------------------------------------------------- 1 | import { fetchEntity, getEntity } from './common.utils' 2 | 3 | /** 4 | * Show View Utils - A function used to create utilities 5 | * 6 | * @param {String} resourceName The name of the resource 7 | * @param {Object} store The global Vuex store variable 8 | * @param {Object} router The global Vue router variable 9 | * 10 | * @return {Object} A set of functions to be used in a Show form. 11 | */ 12 | export default ({ resourceName, store, router }) => { 13 | return { 14 | /** 15 | * getEntity - Gets a 'resourceName' entity from the store. 16 | * 17 | * @return {Object} A 'resourceName' entity. 18 | */ 19 | getEntity() { 20 | return getEntity({ router, resourceName, store }) 21 | }, 22 | 23 | /** 24 | * fetchEntity - Fetchs a single 'resourceName' element from the store. 25 | * 26 | * @return {Object} A fetched 'resourceName' entity. 27 | */ 28 | fetchEntity() { 29 | return fetchEntity({ resourceName, router, store }) 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/main.sass: -------------------------------------------------------------------------------- 1 | $font-stack: 'Montserrat', Helvetica, Arial 2 | 3 | body 4 | font: 100% $font-stack 5 | 6 | @import '~vuetify/src/styles/main.sass' 7 | -------------------------------------------------------------------------------- /src/templates/src/en/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "UNDEFINED_PROPERTY": "{prefix} It seems that the {prop} property is undefined.\n\n", 3 | "INVALID_SCHEMA": "{prefix} Some of the {prop} properties are invalid:\n{details}\n\n" 4 | } 5 | -------------------------------------------------------------------------------- /src/templates/src/index.js: -------------------------------------------------------------------------------- 1 | const docsUrl = require('@/../package.json').directories.doc 2 | 3 | const errorTitle = '\n\nVueAdmin/{at}:\n\n' 4 | const errorFooter = 5 | '{errorMessage}\tTake a look at our documentation at {url}\n' 6 | 7 | // Component doc paths should be added here 8 | const componentsDocs = { 9 | Admin: '{docsUrl}/Admin/Admin.md#props', 10 | Resource: '{docsUrl}/Resource/Resource.md#props', 11 | DateField: '{docsUrl}/Ui-Components/DateField.md', 12 | } 13 | 14 | /** 15 | * withParams - Defines the interpolation symbol of a template 16 | */ 17 | function withParams(key) { 18 | return `{${key}}` 19 | } 20 | 21 | /** 22 | * buildMessage - Given a message and a set of parameters, interpolates the 23 | * string to return another message 24 | * 25 | * @param {String} message A string containing a template message 26 | * @param {Object} args A set of properties to complete a template message 27 | * 28 | * @return {String} A message built with args 29 | */ 30 | function buildMessage(message, args) { 31 | const paramKeys = Object.keys(args) 32 | return paramKeys.reduce((parsedMessage, paramKey) => { 33 | return parsedMessage.replace(withParams(paramKey), args[paramKey]) 34 | }, message) 35 | } 36 | 37 | /** 38 | * Templates - Given the name of a resource, returns a builder function to 39 | * create error messages. 40 | * 41 | * @param {String} template A string containing the language and type of error 42 | * of a template, e.g.: 'en.error' 43 | * 44 | * @return {Function} A builder function of messageType 45 | */ 46 | export default template => { 47 | const params = template.split('.') 48 | const language = params[0] 49 | const messageType = params[1] 50 | const messages = require(`./${language}/${messageType}.json`) 51 | 52 | /** 53 | * buildErrorMessage - Given a template constant and a set of params, returns 54 | * an error message 55 | * 56 | * @param {String} constant The name of a template message 57 | * @param {Object} messageParams An object containing params to fill a constant 58 | * 59 | * @return {String} An error message built with messageParams 60 | */ 61 | function buildErrorMessage(constant, messageParams) { 62 | const { at } = messageParams 63 | const componentDoc = componentsDocs[at] 64 | const prefix = buildMessage(errorTitle, { at }) 65 | const url = buildMessage(componentDoc, { docsUrl }) 66 | Object.assign(messageParams, { prefix }) 67 | const errorMessage = buildMessage(messages[constant], messageParams) 68 | 69 | return buildMessage(errorFooter, { errorMessage, url }) 70 | } 71 | 72 | const messageTypes = { 73 | error: buildErrorMessage, 74 | // newMessages: buildNewMessage 75 | } 76 | 77 | return messageTypes[messageType] 78 | } 79 | -------------------------------------------------------------------------------- /src/va-auth/src/store/index.js: -------------------------------------------------------------------------------- 1 | import createActions from './modules/actions' 2 | import createGetters from './modules/getters' 3 | import createMutations from './modules/mutations' 4 | import createState from './modules/state' 5 | import Types from '../types' 6 | 7 | /** 8 | * Create Auth Module - Given a set of data, creates a vuex auth module and 9 | * calls the store to get it registered. 10 | * 11 | * @param {Function} client An http client 12 | * @param {String} moduleName The name of the auth module 13 | * @param {Object} store The global Vuex store variable 14 | */ 15 | export default ({ client }) => { 16 | const types = Types 17 | return { 18 | namespaced: true, 19 | state: createState(), 20 | actions: createActions({ client, types }), 21 | mutations: createMutations({ types }), 22 | getters: createGetters({ types }), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/va-auth/src/store/modules/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Auth Module Actions - Given a set of data, creates the actions for an 3 | * auth store module 4 | * 5 | * @param {Object} client An http client 6 | * @param {Object} types An object containing all the auth Types 7 | * 8 | * @return {Object} The actions for the auth store 9 | */ 10 | export default ({ client, types }) => { 11 | return { 12 | [types.AUTH_LOGIN_REQUEST]: ({ commit }, user) => { 13 | commit(types.AUTH_LOGIN_REQUEST) 14 | return client(types.AUTH_LOGIN_REQUEST, { ...user }) 15 | .then(response => { 16 | commit(types.AUTH_LOGIN_SUCCESS, response) 17 | }) 18 | .catch(error => { 19 | commit(types.AUTH_LOGIN_FAILURE, error) 20 | }) 21 | }, 22 | 23 | [types.AUTH_LOGOUT_REQUEST]: ({ commit }) => { 24 | commit(types.AUTH_LOGOUT_REQUEST) 25 | client(types.AUTH_LOGOUT_REQUEST) 26 | .then(() => { 27 | commit(types.AUTH_LOGOUT_SUCCESS) 28 | }) 29 | .catch(error => { 30 | commit(types.AUTH_LOGIN_FAILURE, error) 31 | }) 32 | }, 33 | 34 | [types.AUTH_CHECK_REQUEST]: ({ commit }) => { 35 | commit(types.AUTH_CHECK_REQUEST) 36 | client(types.AUTH_CHECK_REQUEST) 37 | .then(response => { 38 | commit(types.AUTH_CHECK_SUCCESS, response) 39 | }) 40 | .catch(error => { 41 | commit(types.AUTH_CHECK_FAILURE, error) 42 | }) 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/va-auth/src/store/modules/getters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Auth Module Getters - Creates the getters for an auth store module 3 | * 4 | * @return {Object} The getters for the auth store 5 | */ 6 | export default ({ types }) => { 7 | return { 8 | [types.AUTH_GET_STATUS]: state => state.status, 9 | [types.AUTH_IS_AUTHENTICATED]: state => state.isAuthenticated, 10 | [types.AUTH_GET_USER]: state => state.user, 11 | [types.AUTH_GET_ERROR]: state => state.error, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/va-auth/src/store/modules/mutations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Auth Module Mutations - Given a set of data, creates the mutations 3 | * for an auth store module 4 | * 5 | * @param {Object} types An object containing all the auth Types 6 | * 7 | * @return {Object} The mutations for the auth store 8 | */ 9 | export default ({ types }) => { 10 | return { 11 | [types.AUTH_LOGIN_REQUEST]: state => { 12 | state.error = '' 13 | state.status = 'running' 14 | }, 15 | [types.AUTH_LOGIN_SUCCESS]: (state, user) => { 16 | state.isAuthenticated = true 17 | state.status = 'idle' 18 | state.user = user 19 | }, 20 | [types.AUTH_LOGIN_FAILURE]: (state, error) => { 21 | state.isAuthenticated = false 22 | state.error = error 23 | state.status = 'idle' 24 | }, 25 | 26 | [types.AUTH_LOGOUT_REQUEST]: state => { 27 | state.error = '' 28 | state.status = 'running' 29 | }, 30 | [types.AUTH_LOGOUT_SUCCESS]: state => { 31 | state.isAuthenticated = false 32 | state.status = 'idle' 33 | state.user = {} 34 | }, 35 | [types.AUTH_LOGOUT_FAILURE]: (state, error) => { 36 | state.error = error 37 | state.status = 'idle' 38 | }, 39 | 40 | [types.AUTH_CHECK_REQUEST]: state => { 41 | state.error = '' 42 | state.status = 'running' 43 | }, 44 | [types.AUTH_CHECK_SUCCESS]: (state, user) => { 45 | state.isAuthenticated = true 46 | state.status = 'idle' 47 | state.user = user 48 | }, 49 | [types.AUTH_CHECK_FAILURE]: (state, error) => { 50 | state.error = error 51 | state.isAuthenticated = false 52 | state.status = 'idle' 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/va-auth/src/store/modules/state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Auth Module State - Creates the state for an auth store module 3 | * 4 | * @return {Object} The state for the auth store 5 | */ 6 | export default () => { 7 | return { 8 | error: '', 9 | isAuthenticated: false, 10 | status: 'idle', 11 | user: {}, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/va-auth/src/types/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespace: 'auth', 3 | 4 | /** 5 | * Actions and Mutations 6 | */ 7 | 8 | AUTH_LOGIN_REQUEST: 'AUTH_LOGIN_REQUEST', 9 | AUTH_LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS', 10 | AUTH_LOGIN_FAILURE: 'AUTH_LOGIN_FAILURE', 11 | 12 | AUTH_LOGOUT_REQUEST: 'AUTH_LOGOUT_REQUEST', 13 | AUTH_LOGOUT_SUCCESS: 'AUTH_LOGOUT_SUCCESS', 14 | AUTH_LOGOUT_FAILURE: 'AUTH_LOGOUT_FAILURE', 15 | 16 | AUTH_CHECK_REQUEST: 'AUTH_CHECK_REQUEST', 17 | AUTH_CHECK_SUCCESS: 'AUTH_CHECK_SUCCESS', 18 | AUTH_CHECK_FAILURE: 'AUTH_CHECK_FAILURE', 19 | 20 | /** 21 | * Getters 22 | */ 23 | 24 | AUTH_GET_USER: 'AUTH_GET_USER', 25 | AUTH_GET_STATUS: 'AUTH_GET_STATUS', 26 | AUTH_GET_ERROR: 'AUTH_GET_ERROR', 27 | AUTH_IS_AUTHENTICATED: 'AUTH_IS_AUTHENTICATED', 28 | } 29 | -------------------------------------------------------------------------------- /src/validators/src/components/resource.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | function formatResult(result) { 4 | if (result.error) { 5 | const { name, details } = result.error 6 | const error = { name, details } 7 | return { error } 8 | } 9 | return result.value 10 | } 11 | 12 | /** 13 | * validateRedirect - Given an object checks it has a valid redirect schema 14 | * 15 | * @param {Object} redirect An object 16 | * 17 | * @return {Object} A Joi object with the validation review 18 | */ 19 | export const validateRedirect = redirect => { 20 | const joiResult = Joi.object() 21 | .keys({ 22 | views: Joi.object().keys({ 23 | create: Joi.string().valid(['edit', 'list', 'show']), 24 | edit: Joi.string().valid(['create', 'list', 'show']), 25 | }), 26 | }) 27 | .validate(redirect) 28 | return formatResult(joiResult) 29 | } 30 | -------------------------------------------------------------------------------- /src/validators/src/index.js: -------------------------------------------------------------------------------- 1 | import { validateRedirect } from './components/resource' 2 | 3 | function validateSchema(prop, schema) { 4 | const validations = { 5 | redirect: validateRedirect, 6 | // add more properties here, binded with a validation function 7 | } 8 | return validations[prop](schema) 9 | } 10 | 11 | export { validateSchema } 12 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | mocha: true, 5 | 'cypress/globals': true, 6 | }, 7 | rules: { 8 | strict: 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/factory/auth/index.js: -------------------------------------------------------------------------------- 1 | import { createUser } from '../users' 2 | 3 | export const createAuthResponse = (args = {}) => { 4 | const status = 200 5 | const user = createUser() 6 | const _args = { 7 | status, 8 | user, 9 | } 10 | return Object.assign({}, _args, args) 11 | } 12 | 13 | export const createCredentials = (args = {}) => { 14 | const username = 'dev@camba.coop' 15 | const password = '123456' 16 | const _args = { 17 | username, 18 | password, 19 | } 20 | return Object.assign({}, _args, args) 21 | } 22 | -------------------------------------------------------------------------------- /tests/e2e/factory/env/index.js: -------------------------------------------------------------------------------- 1 | export const createApiUrl = ({ url, port, route }) => { 2 | const address = {} 3 | address.url = url || 'http://localhost' 4 | address.port = port || '8080' 5 | address.route = route || '' 6 | return `${address.url}:${address.port}/${address.route}` 7 | } 8 | -------------------------------------------------------------------------------- /tests/e2e/factory/index.js: -------------------------------------------------------------------------------- 1 | import { createArticle, createMagazine, createAuthor } from './resources' 2 | import { createAuthResponse, createCredentials } from './auth' 3 | import { createUser } from './users' 4 | import { 5 | createInitialVuexStoreGetters, 6 | createInitialVuexStoreState, 7 | } from './store' 8 | import { createApiUrl } from './env' 9 | 10 | export default { 11 | // Auth builders 12 | createAuthResponse, 13 | createCredentials, 14 | // Entities builders 15 | createArticle, 16 | createMagazine, 17 | createAuthor, 18 | // Env builders 19 | createApiUrl, 20 | // Store builders 21 | createInitialVuexStoreState, 22 | createInitialVuexStoreGetters, 23 | // User builders 24 | createUser, 25 | } 26 | -------------------------------------------------------------------------------- /tests/e2e/factory/resources/articles.js: -------------------------------------------------------------------------------- 1 | import { ipsum } from '../utils' 2 | 3 | export const createArticle = (args = {}) => { 4 | // Shortens the paragraph 5 | const title = ipsum.generateSentence() 6 | const content = ipsum.generateParagraph({ useStartingSentence: true }) 7 | const _args = { 8 | title, 9 | content, 10 | } 11 | return Object.assign({}, _args, args) 12 | } 13 | -------------------------------------------------------------------------------- /tests/e2e/factory/resources/authors.js: -------------------------------------------------------------------------------- 1 | import { ipsum, randomDate } from '../utils' 2 | 3 | export const createAuthor = (args = {}) => { 4 | // Shortens the paragraph 5 | const name = ipsum.generateWord() 6 | const lastname = ipsum.generateWord() 7 | const birthdate = randomDate( 8 | new Date(1970, 1, 1, 0, 0, 0, 0), 9 | new Date(1980, 1, 1, 0, 0, 0, 0) 10 | ).toISOString() 11 | const _args = { 12 | name, 13 | lastname, 14 | birthdate, 15 | } 16 | return Object.assign({}, _args, args) 17 | } 18 | -------------------------------------------------------------------------------- /tests/e2e/factory/resources/index.js: -------------------------------------------------------------------------------- 1 | import { createArticle } from './articles' 2 | import { createMagazine } from './magazines' 3 | import { createAuthor } from './authors' 4 | 5 | export { createArticle, createMagazine, createAuthor } 6 | -------------------------------------------------------------------------------- /tests/e2e/factory/resources/magazines.js: -------------------------------------------------------------------------------- 1 | import { ipsum, numbers } from '../utils' 2 | 3 | export const createMagazine = (args = {}) => { 4 | const name = ipsum.generateSentence() 5 | const issue = `#${numbers.randomBetween(1, 500)}` 6 | const publisher = ipsum.generateParagraph(1, { useStartingSentence: true }) 7 | const _args = { 8 | name, 9 | issue, 10 | publisher, 11 | } 12 | return Object.assign({}, _args, args) 13 | } 14 | -------------------------------------------------------------------------------- /tests/e2e/factory/store/common.utils.js: -------------------------------------------------------------------------------- 1 | export const initialResourcesRoutes = resources => { 2 | return resources.map(resource => { 3 | return { 4 | path: `/${resource}`, 5 | name: resource, 6 | } 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /tests/e2e/factory/store/index.js: -------------------------------------------------------------------------------- 1 | import createInitialVuexStoreState from './initial.state' 2 | import createInitialVuexStoreGetters from './initial.getters' 3 | 4 | export { createInitialVuexStoreState, createInitialVuexStoreGetters } 5 | -------------------------------------------------------------------------------- /tests/e2e/factory/store/initial.state.js: -------------------------------------------------------------------------------- 1 | import { initialResourcesRoutes } from './common.utils' 2 | 3 | /** 4 | * Anonymous Function - Creates a simualtion of initial vuex crud state 5 | * 6 | * @return {Object} The expected Vuex Crud mocked state 7 | */ 8 | export default () => { 9 | // Initial vuex crud resources should be added here 10 | const initialResources = ['articles', 'magazines', 'authors'] 11 | // Vuex Crud Initial State for a resource 12 | const initialResourceState = { 13 | createError: null, 14 | destroyError: null, 15 | entities: {}, 16 | fetchListError: null, 17 | fetchSingleError: null, 18 | isCreating: false, 19 | isDestroying: false, 20 | isFetchingList: false, 21 | isFetchingSingle: false, 22 | isReplacing: false, 23 | isUpdating: false, 24 | list: [], 25 | replaceError: null, 26 | updateError: null, 27 | } 28 | // Vuex Initial State for alerts 29 | const initialAlertsState = { 30 | color: '', 31 | isVisible: false, 32 | text: '', 33 | } 34 | // Vuex Initial State for auth 35 | const initialAuthState = { 36 | error: '', 37 | isAuthenticated: false, 38 | status: 'idle', 39 | user: {}, 40 | } 41 | 42 | // Vuex Initial State for request 43 | const initialRequestState = { 44 | isLoading: false, 45 | } 46 | // Vuex Initial State for entities 47 | const initialEntitiesState = {} 48 | // Vuex Initial State for resource routes 49 | const initialResourcesState = { 50 | routes: initialResourcesRoutes(initialResources), 51 | } 52 | 53 | /** 54 | * initResourcesCrud - Given a list of resources, creates mocked vuex crud 55 | * state for each of them 56 | * 57 | * @param {Array} resources An array of strings 58 | * 59 | * @return {Object} An object with mocked vuex crud state 60 | */ 61 | function initResourcesState(resources) { 62 | const _resources = {} 63 | resources.forEach(resource => { 64 | _resources[resource] = initialResourceState 65 | }) 66 | return _resources 67 | } 68 | 69 | return { 70 | ...initResourcesState(initialResources), 71 | alerts: initialAlertsState, 72 | auth: initialAuthState, 73 | entities: initialEntitiesState, 74 | resources: initialResourcesState, 75 | requests: initialRequestState, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/e2e/factory/users/index.js: -------------------------------------------------------------------------------- 1 | export const createUser = (args = {}) => { 2 | const id = 234567 3 | const email = 'dev@camba.coop' 4 | const permissions = ['admin'] 5 | const _args = { 6 | id, 7 | email, 8 | permissions, 9 | } 10 | return Object.assign({}, _args, args) 11 | } 12 | -------------------------------------------------------------------------------- /tests/e2e/factory/utils.js: -------------------------------------------------------------------------------- 1 | const Ipsum = require('bavaria-ipsum') 2 | 3 | export const ipsum = new Ipsum({ 4 | startSentence: 'Vue Admin aspera iaspis', 5 | minSentenceWords: 2, 6 | maxSentenceWords: 6, 7 | minParagraphSentences: 1, 8 | maxParagraphSentences: 3, 9 | }) 10 | 11 | export const numbers = { 12 | randomBetween(min, max) { 13 | return Math.floor(Math.random() * (max - min + 1) + min) 14 | }, 15 | } 16 | 17 | export const randomDate = (start, end) => { 18 | return new Date( 19 | start.getTime() + Math.random() * (end.getTime() - start.getTime()) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /tests/e2e/fixtures/authors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1628157, 4 | "name": "Jerónimo", 5 | "lastname": "Zepeda", 6 | "birthdate": "2018-06-27T22:41:40.611Z" 7 | }, 8 | { 9 | "id": 2474107, 10 | "name": "Graciela", 11 | "lastname": "Solorio", 12 | "birthdate": "2019-04-13T08:03:23.089Z" 13 | }, 14 | { 15 | "id": 453660, 16 | "name": "Roberto", 17 | "lastname": "Saldivar", 18 | "birthdate": "2018-07-28T08:57:55.461Z" 19 | }, 20 | { 21 | "id": 655968, 22 | "name": "María Luisa", 23 | "lastname": "Archuleta", 24 | "birthdate": "2018-07-30T13:50:05.975Z" 25 | }, 26 | { 27 | "id": 2168222, 28 | "name": "Isabel", 29 | "lastname": "Acuña", 30 | "birthdate": "2018-12-24T05:26:21.284Z" 31 | }, 32 | { 33 | "id": 110552, 34 | "name": "Yolanda", 35 | "lastname": "Bustos", 36 | "birthdate": "2018-10-16T13:14:41.552Z" 37 | }, 38 | { 39 | "id": 508428, 40 | "name": "Julio César", 41 | "lastname": "Terrazas", 42 | "birthdate": "2019-02-22T11:38:51.171Z" 43 | }, 44 | { 45 | "id": 2349762, 46 | "name": "Sara", 47 | "lastname": "Villa", 48 | "birthdate": "2018-07-10T16:38:49.125Z" 49 | }, 50 | { 51 | "id": 726880, 52 | "name": "Emilia", 53 | "lastname": "Merino", 54 | "birthdate": "2018-11-21T22:10:32.339Z" 55 | }, 56 | { 57 | "id": 2392010, 58 | "name": "Adela", 59 | "lastname": "Esquivel", 60 | "birthdate": "2019-03-06T03:05:58.655Z" 61 | }, 62 | { 63 | "id": 590988, 64 | "name": "Gilberto", 65 | "lastname": "Delagarza", 66 | "birthdate": "2018-09-24T15:35:27.656Z" 67 | }, 68 | { 69 | "id": 2003453, 70 | "name": "Luisa", 71 | "lastname": "Urrutia", 72 | "birthdate": "2018-06-17T11:14:44.059Z" 73 | }, 74 | { 75 | "id": 2012058, 76 | "name": "Francisca", 77 | "lastname": "Abreu", 78 | "birthdate": "2018-10-29T07:58:22.281Z" 79 | }, 80 | { 81 | "id": 315226, 82 | "name": "Rafael", 83 | "lastname": "Armas", 84 | "birthdate": "2019-01-10T01:49:31.995Z" 85 | }, 86 | { 87 | "id": 1409776, 88 | "name": "Lucas", 89 | "lastname": "Ramón", 90 | "birthdate": "2018-12-04T20:43:30.497Z" 91 | }, 92 | { 93 | "id": 2058727, 94 | "name": "Estela", 95 | "lastname": "Marroquín", 96 | "birthdate": "2018-12-08T18:24:05.609Z" 97 | }, 98 | { 99 | "id": 566659, 100 | "name": "David", 101 | "lastname": "Cornejo", 102 | "birthdate": "2018-10-06T01:34:52.379Z" 103 | }, 104 | { 105 | "id": 24249, 106 | "name": "Federico", 107 | "lastname": "Jurado", 108 | "birthdate": "2019-01-13T06:52:42.200Z" 109 | }, 110 | { 111 | "id": 1473716, 112 | "name": "Lucas", 113 | "lastname": "Leyva", 114 | "birthdate": "2018-11-28T14:01:36.224Z" 115 | } 116 | ] 117 | -------------------------------------------------------------------------------- /tests/e2e/helpers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cypress Helpers 3 | */ 4 | 5 | // TODO: deprecate this file and use ../lib/helpers instead ^_^ 6 | export default { 7 | queryElementByProp: ({ type = '', prop, value }) => 8 | `${type}[${prop}=${value}]`, 9 | } 10 | -------------------------------------------------------------------------------- /tests/e2e/lib/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Command Helpers 3 | */ 4 | 5 | export default { 6 | createElementQueryWith: ({ type = '', prop, value }) => 7 | `${type}[${prop}=${value}]`, 8 | 9 | createUrlWithResource: ({ resourceName, path = '' }) => 10 | `${Cypress.config().baseUrl}#/${resourceName}/${path}`, 11 | } 12 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | module.exports = (on, config) => { 4 | return Object.assign({}, config, { 5 | fixturesFolder: 'tests/e2e/fixtures', 6 | integrationFolder: 'tests/e2e/specs', 7 | screenshotsFolder: 'tests/e2e/screenshots', 8 | videosFolder: 'tests/e2e/videos', 9 | supportFile: 'tests/e2e/support/index.js', 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /tests/e2e/specs/articles/create.spec.js: -------------------------------------------------------------------------------- 1 | const Factory = require('../../factory') 2 | const UI_CONTENT = require('../../../../src/constants/ui.content.default') 3 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 4 | 5 | describe('Articles: Create Test', () => { 6 | const resourceName = 'articles' 7 | const view = 'create' 8 | const article = {} 9 | 10 | before('Initialises authenticated with a default user', () => { 11 | cy.InitAuthenticatedUser() 12 | }) 13 | 14 | before('Visits the create url', () => { 15 | cy.visit(`/#/${resourceName}/${view}`) 16 | }) 17 | 18 | before('Generate an article to create', () => { 19 | Object.assign(article, Factory.createArticle()) 20 | }) 21 | 22 | it('The url path should be articles/create', () => { 23 | cy.url().should('include', `${resourceName}/${view}`) 24 | }) 25 | 26 | it('Articles Create View should render title: Articles', () => { 27 | const createViewTitleText = 'Create Article' 28 | const createViewTitleContainer = cy.getElement({ 29 | constant: UI_NAMES.RESOURCE_VIEW_CONTAINER_TITLE, 30 | constantParams: { resourceName, view }, 31 | elementType: 'div', 32 | elementProp: 'name', 33 | }) 34 | 35 | createViewTitleContainer.should('contain', createViewTitleText) 36 | }) 37 | 38 | it('The {Title} input is filled when an user types in', () => { 39 | theFieldInputIsFilledWhenAnUserTypesIn('title') 40 | }) 41 | 42 | it('The {Content} input is filled when an user types in', () => { 43 | theFieldInputIsFilledWhenAnUserTypesIn('content') 44 | }) 45 | 46 | it('Articles Create View should redirect to the List View on a create submit', () => { 47 | const routes = [{ name: view, response: article }, { name: 'list' }] 48 | cy.InitServer({ resourceName, routes, response: article }) 49 | 50 | const submitButtonText = UI_CONTENT.CREATE_SUBMIT_BUTTON 51 | const submitButton = cy.getElement({ 52 | constant: UI_NAMES.RESOURCE_VIEW_SUBMIT_BUTTON, 53 | constantParams: { resourceName, view }, 54 | elementType: 'button', 55 | elementProp: 'name', 56 | }) 57 | 58 | submitButton.should('contain', submitButtonText).click() 59 | 60 | cy.server({ enable: false }) 61 | 62 | cy.url().should('include', '/articles') 63 | }) 64 | 65 | function theFieldInputIsFilledWhenAnUserTypesIn(field) { 66 | const input = cy.getElement({ 67 | constant: UI_NAMES.RESOURCE_VIEW_ELEMENT_FIELD, 68 | constantParams: { resourceName, view, field }, 69 | elementType: 'input', 70 | elementProp: 'name', 71 | }) 72 | 73 | input.type(article[field]) 74 | input.should('have.value', article[field]) 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /tests/e2e/specs/articles/delete.spec.js: -------------------------------------------------------------------------------- 1 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 2 | 3 | describe('Articles: Delete Test', () => { 4 | const resourceName = 'articles' 5 | const article = {} 6 | 7 | before('Initialises authenticated with a default user', () => { 8 | cy.InitAuthenticatedUser() 9 | }) 10 | 11 | before('Search an article to delete', () => { 12 | cy.fixture(resourceName).then(fixture => { 13 | Object.assign(article, fixture[0]) 14 | }) 15 | }) 16 | 17 | before('Initialises the server', () => { 18 | const routes = [{ name: 'show', response: article }] 19 | cy.InitServer({ resourceName, routes }) 20 | }) 21 | 22 | before('Visits the Show view url', () => { 23 | const showUrl = `${resourceName}/show/${article.id}` 24 | cy.visit(`/#/${showUrl}`) 25 | cy.url().should('include', showUrl) 26 | cy.server({ enable: false }) 27 | }) 28 | 29 | it('Press the delete button in the Show view', () => { 30 | const routes = [{ name: 'delete', response: article }, { name: 'list' }] 31 | cy.InitServer({ resourceName, routes }) 32 | const deleteButton = cy.getElement({ 33 | constant: UI_NAMES.RESOURCE_DELETE_BUTTON, 34 | constantParams: { resourceName }, 35 | elementType: 'button', 36 | elementProp: 'name', 37 | }) 38 | 39 | deleteButton.click() 40 | cy.server({ enable: false }) 41 | 42 | cy.wait(`@${resourceName}/delete/${article.id}`).then(xmlHttpRequest => { 43 | expect(xmlHttpRequest.status).equal(202) 44 | cy.url().should('include', `/${resourceName}`) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/e2e/specs/articles/show.spec.js: -------------------------------------------------------------------------------- 1 | const { queryElementByProp } = require('../../helpers') 2 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 3 | 4 | describe('Articles: Show Test', () => { 5 | const resourceName = 'articles' 6 | const view = 'show' 7 | const article = {} 8 | 9 | before('Initialises authenticated with a default user', () => { 10 | cy.InitAuthenticatedUser() 11 | }) 12 | 13 | before('Search an article to show', () => { 14 | cy.fixture(resourceName).then(fixture => { 15 | // Takes the first element of the fixture to use as subject 16 | Object.assign(article, fixture[0]) 17 | }) 18 | }) 19 | 20 | before('Initialises the server', () => { 21 | // Inits the server with a stubbed get endpoint 22 | const routes = [{ name: view, response: article }] 23 | cy.InitServer({ resourceName, routes }) 24 | }) 25 | 26 | it('Visits the articles show', () => { 27 | // Exercise: visits the show view 28 | cy.visit(`/#/${resourceName}/${view}/${article.id}`) 29 | cy.server({ enable: false }) 30 | // Assertion: the url should match the show view url 31 | cy.url().should('include', `${resourceName}/${view}/${article.id}`) 32 | }) 33 | 34 | it('Articles Show View should render title: Articles', () => { 35 | const showViewTitleText = 'Article' 36 | 37 | cy.getElement({ 38 | constant: UI_NAMES.RESOURCE_VIEW_CONTAINER_TITLE, 39 | constantParams: { resourceName, view }, 40 | elementType: 'div', 41 | elementProp: 'name', 42 | }).should('contain', showViewTitleText) 43 | }) 44 | 45 | it('Articles Show View should contain the title field', () => { 46 | articlesShowViewShouldContainTheField('title') 47 | }) 48 | 49 | it('Articles Show View should contain the content field', () => { 50 | articlesShowViewShouldContainTheField('content') 51 | }) 52 | 53 | /** 54 | * Helper functions 55 | **/ 56 | function queryToElementWith(containerType, containerParams) { 57 | const containerName = UI_NAMES[containerType].with(containerParams) 58 | return queryElementByProp({ 59 | type: 'div', 60 | prop: 'name', 61 | value: containerName, 62 | }) 63 | } 64 | 65 | function queryToElement(containerType) { 66 | return queryToElementWith(containerType, { resourceName, view }) 67 | } 68 | 69 | function articlesShowViewShouldContainTheField(field) { 70 | cy.get(queryToElement('RESOURCE_VIEW_CONTAINER_FIELDS')).should( 71 | fieldsContainerRes => { 72 | const fieldContainerElement = queryToElementWith( 73 | 'RESOURCE_VIEW_CONTAINER_FIELD', 74 | { 75 | resourceName, 76 | view, 77 | field, 78 | } 79 | ) 80 | const fieldContainer = fieldsContainerRes.find(fieldContainerElement) 81 | expect(fieldContainer).to.contain(article[field]) 82 | } 83 | ) 84 | } 85 | }) 86 | -------------------------------------------------------------------------------- /tests/e2e/specs/auth/auth.spec.js: -------------------------------------------------------------------------------- 1 | const UI_CONTENT = require('../../../../src/constants/ui.content.default') 2 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 3 | 4 | describe('Auth Test', () => { 5 | const view = 'login' 6 | const user = { 7 | username: 'dev@camba.coop', 8 | password: '123456', 9 | } 10 | 11 | const findInput = ({ constant }) => 12 | cy.getElement({ constant, elementType: 'input', elementProp: 'name' }) 13 | 14 | const findButton = ({ constant }) => 15 | cy.getElement({ constant, elementType: 'button', elementProp: 'name' }) 16 | 17 | const findTypeAndAssert = ({ element, value, condition }) => { 18 | const input = findInput({ constant: element }) 19 | input.type(value) 20 | input.should(condition, value) 21 | } 22 | 23 | beforeEach('Visits the auth url', () => { 24 | cy.visit(`/#/${view}`) 25 | }) 26 | 27 | it('The url path should be /login', () => { 28 | cy.url().should('include', `/${view}`) 29 | }) 30 | 31 | it('Login View should render a title: Sign In', () => { 32 | const createViewTitleText = UI_CONTENT.AUTH_CONTAINER_TITLE 33 | const createViewTitleContainer = cy.getElement({ 34 | constant: UI_NAMES.AUTH_CONTAINER_TITLE, 35 | elementType: 'div', 36 | elementProp: 'name', 37 | }) 38 | 39 | createViewTitleContainer.should('contain', createViewTitleText) 40 | }) 41 | 42 | it('The {username} input is filled when a user types in', () => { 43 | findTypeAndAssert({ 44 | element: UI_NAMES.AUTH_USERNAME_INPUT, 45 | value: user.username, 46 | condition: 'have.value', 47 | }) 48 | }) 49 | 50 | it('The {password} input is filled when a user types in', () => { 51 | findTypeAndAssert({ 52 | element: UI_NAMES.AUTH_PASSWORD_INPUT, 53 | value: user.password, 54 | condition: 'have.value', 55 | }) 56 | }) 57 | 58 | it('The Sign In button is disabled when no username and password were given', () => { 59 | const button = findButton({ constant: UI_NAMES.AUTH_SIGN_IN_BUTTON }) 60 | button.should('be.disabled') 61 | }) 62 | 63 | it('When a user types a valid username and password, the button is enabled', () => { 64 | findTypeAndAssert({ 65 | element: UI_NAMES.AUTH_USERNAME_INPUT, 66 | value: user.username, 67 | condition: 'have.value', 68 | }) 69 | findTypeAndAssert({ 70 | element: UI_NAMES.AUTH_PASSWORD_INPUT, 71 | value: user.password, 72 | condition: 'have.value', 73 | }) 74 | const button = findButton({ constant: UI_NAMES.AUTH_SIGN_IN_BUTTON }) 75 | button.should('be.enabled') 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /tests/e2e/specs/magazines/show.spec.js: -------------------------------------------------------------------------------- 1 | const { InitEntityUtils } = require('../../lib/commands') 2 | 3 | describe('Magazines: Show Action Test', () => { 4 | const resourceName = 'magazines' 5 | const view = 'show' 6 | const magazine = {} 7 | const utils = InitEntityUtils({ 8 | resourceName, 9 | view, 10 | }) 11 | 12 | before('Initialises authenticated with a default user', () => { 13 | cy.InitAuthenticatedUser() 14 | }) 15 | 16 | before('Search a magazine to show', () => { 17 | cy.fixture(resourceName).then(fixture => { 18 | // Takes the first element of the fixture to use as subject 19 | Object.assign(magazine, fixture[0]) 20 | }) 21 | }) 22 | 23 | before('Initialises the server', () => { 24 | // Inits the server with a stubbed get endpoint 25 | const routes = [{ name: view, response: magazine }] 26 | cy.InitServer({ resourceName, routes }) 27 | }) 28 | 29 | it('Visits the magazines show url and the path should be magazines/show/:id', () => { 30 | // Exercise: visits the show view 31 | cy.visit(`/#/${resourceName}/${view}/${magazine.id}`) 32 | cy.server({ enable: false }) 33 | // Assertion: the url should match the show view url 34 | cy.url().should('include', `${resourceName}/${view}/${magazine.id}`) 35 | }) 36 | 37 | it('The {Name} input should match the created magazine {name}', () => { 38 | // Setup: Gets the 'name' input element 39 | const input = utils.getInputBy({ field: 'name' }) 40 | // Assertion: the input contains the magazine issue content 41 | input.should('have.value', magazine.name) 42 | }) 43 | 44 | it('The {Issue} input should match the created magazine {issue}', () => { 45 | // Setup: Gets the 'issue' input element 46 | const input = utils.getInputBy({ field: 'issue' }) 47 | // Assertion: the input contains the magazine issue content 48 | input.should('have.value', magazine.issue) 49 | }) 50 | 51 | it('The {Publisher} input should match the created magazine {publisher}', () => { 52 | // Setup: Gets the 'publisher' input element 53 | const input = utils.getInputBy({ field: 'publisher' }) 54 | // Assertion: the input contains the magazine publisher content 55 | input.should('have.value', magazine.publisher) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /tests/e2e/specs/spinner/spinner-create.spec.js: -------------------------------------------------------------------------------- 1 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 2 | import { Types as RequestsTypes } from '../../../../src/store/modules/requests' 3 | 4 | describe('Spinner on a Create View Test', () => { 5 | const testResourceName = 'articles' 6 | const view = 'create' 7 | 8 | before('Initialises authenticated with a default user', () => { 9 | cy.InitAuthenticatedUser() 10 | }) 11 | 12 | before('Visits the create url', () => { 13 | cy.visit(`/#/${testResourceName}/${view}`) 14 | }) 15 | 16 | it('The spinner should not be visualized when the store property isLoading is set to false', () => { 17 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 18 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 19 | const isLoading = false 20 | cy.getStore().invoke('commit', mutation, { isLoading }) 21 | 22 | const spinnerContainer = cy.getElement({ 23 | constant: UI_NAMES.SPINNER_CONTAINER, 24 | elementType: 'div', 25 | elementProp: 'id', 26 | }) 27 | 28 | spinnerContainer.should('not.be.visible') 29 | }) 30 | 31 | it('The spinner should be visualized when the store property isLoading is set to true', () => { 32 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 33 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 34 | const isLoading = true 35 | cy.getStore().invoke('commit', mutation, { isLoading }) 36 | 37 | const spinnerContainer = cy.getElement({ 38 | constant: UI_NAMES.SPINNER_CONTAINER, 39 | elementType: 'div', 40 | elementProp: 'id', 41 | }) 42 | 43 | spinnerContainer.should('be.visible') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/e2e/specs/spinner/spinner-edit.spec.js: -------------------------------------------------------------------------------- 1 | import Factory from '../../factory' 2 | import UI_NAMES from '../../../../src/constants/ui.element.names' 3 | import { Types as RequestsTypes } from '../../../../src/store/modules/requests' 4 | 5 | describe('Spinner on a Edit View Test', () => { 6 | const resourceName = 'articles' 7 | const view = 'edit' 8 | const article = {} 9 | const newArticle = Factory.createArticle() 10 | 11 | before('Initialises authenticated with a default user', () => { 12 | cy.InitAuthenticatedUser() 13 | }) 14 | 15 | before('Search an article to edit', () => { 16 | cy.fixture(resourceName).then(fixture => { 17 | Object.assign(article, fixture[0]) 18 | newArticle.id = article.id 19 | }) 20 | }) 21 | 22 | before('Initialises the mocked serve and visits the edit url', () => { 23 | const response = article 24 | const routes = [{ name: 'edit', response }, { name: 'show', response }] 25 | 26 | cy.InitServer({ resourceName, routes }) 27 | cy.visit(`/#/${resourceName}/${view}/${article.id}`) 28 | cy.server({ enable: false }) 29 | }) 30 | 31 | it('The spinner should not be visualized when the store property isLoading is set to false', () => { 32 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 33 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 34 | const isLoading = false 35 | cy.getStore().invoke('commit', mutation, { isLoading }) 36 | 37 | const spinnerContainer = cy.getElement({ 38 | constant: UI_NAMES.SPINNER_CONTAINER, 39 | elementType: 'div', 40 | elementProp: 'id', 41 | }) 42 | 43 | spinnerContainer.should('not.be.visible') 44 | }) 45 | 46 | it('The spinner should be visualized when the store property isLoading is set to true', () => { 47 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 48 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 49 | const isLoading = true 50 | cy.getStore().invoke('commit', mutation, { isLoading }) 51 | 52 | const spinnerContainer = cy.getElement({ 53 | constant: UI_NAMES.SPINNER_CONTAINER, 54 | elementType: 'div', 55 | elementProp: 'id', 56 | }) 57 | 58 | spinnerContainer.should('be.visible') 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /tests/e2e/specs/spinner/spinner-list.spec.js: -------------------------------------------------------------------------------- 1 | import UI_NAMES from '../../../../src/constants/ui.element.names' 2 | import { Types as RequestsTypes } from '../../../../src/store/modules/requests' 3 | 4 | describe('Spinner on a List View Test', () => { 5 | const resourceName = 'articles' 6 | const view = 'list' 7 | 8 | before('Initialises authenticated with a default user', () => { 9 | cy.InitAuthenticatedUser() 10 | }) 11 | 12 | before('Visit the list url', () => { 13 | const routes = [{ name: view }] 14 | cy.InitServer({ resourceName, routes }) 15 | cy.visit(`/#/${resourceName}/`) 16 | }) 17 | 18 | it('The spinner should not be visualized when the store property isLoading is set to false', () => { 19 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 20 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 21 | const isLoading = false 22 | cy.getStore().invoke('commit', mutation, { isLoading }) 23 | 24 | const spinnerContainer = cy.getElement({ 25 | constant: UI_NAMES.SPINNER_CONTAINER, 26 | elementType: 'div', 27 | elementProp: 'id', 28 | }) 29 | 30 | spinnerContainer.should('not.be.visible') 31 | }) 32 | 33 | it('The spinner should be visualized when the store property isLoading is set to true', () => { 34 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 35 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 36 | const isLoading = true 37 | cy.getStore().invoke('commit', mutation, { isLoading }) 38 | 39 | const spinnerContainer = cy.getElement({ 40 | constant: UI_NAMES.SPINNER_CONTAINER, 41 | elementType: 'div', 42 | elementProp: 'id', 43 | }) 44 | 45 | spinnerContainer.should('be.visible') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/e2e/specs/spinner/spinner-show.spec.js: -------------------------------------------------------------------------------- 1 | import UI_NAMES from '../../../../src/constants/ui.element.names' 2 | import { Types as RequestsTypes } from '../../../../src/store/modules/requests' 3 | 4 | describe('Spinner on a Show View Test', () => { 5 | const resourceName = 'articles' 6 | const view = 'show' 7 | const article = {} 8 | 9 | before('Initialises authenticated with a default user', () => { 10 | cy.InitAuthenticatedUser() 11 | }) 12 | 13 | before('Search an article to show', () => { 14 | cy.fixture(resourceName).then(fixture => { 15 | // Takes the first element of the fixture to use as subject 16 | Object.assign(article, fixture[0]) 17 | }) 18 | }) 19 | 20 | before('Initialises the server', () => { 21 | // Inits the server with a stubbed get endpoint 22 | const routes = [{ name: view, response: article }] 23 | cy.InitServer({ resourceName, routes }) 24 | }) 25 | 26 | before('Visit the show url', () => { 27 | cy.visit(`/#/${resourceName}/${view}/${article.id}`) 28 | }) 29 | 30 | it('The spinner should not be visualized when the store property isLoading is set to false', () => { 31 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 32 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 33 | const isLoading = false 34 | cy.getStore().invoke('commit', mutation, { isLoading }) 35 | 36 | const spinnerContainer = cy.getElement({ 37 | constant: UI_NAMES.SPINNER_CONTAINER, 38 | elementType: 'div', 39 | elementProp: 'id', 40 | }) 41 | 42 | spinnerContainer.should('not.be.visible') 43 | }) 44 | 45 | it('The spinner should be visualized when the store property isLoading is set to true', () => { 46 | const { namespace, REQUESTS_SET_LOADING } = RequestsTypes 47 | const mutation = `${namespace}/${REQUESTS_SET_LOADING}` 48 | const isLoading = true 49 | cy.getStore().invoke('commit', mutation, { isLoading }) 50 | 51 | const spinnerContainer = cy.getElement({ 52 | constant: UI_NAMES.SPINNER_CONTAINER, 53 | elementType: 'div', 54 | elementProp: 'id', 55 | }) 56 | 57 | spinnerContainer.should('be.visible') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/e2e/specs/store/state.spec.js: -------------------------------------------------------------------------------- 1 | import Factory from '../../factory' 2 | 3 | describe('Vuex Store State', () => { 4 | const getStore = () => cy.getStore() 5 | const initialState = {} 6 | 7 | before('Initialises the store', () => { 8 | const _initialState = Factory.createInitialVuexStoreState() 9 | Object.assign(initialState, _initialState) 10 | }) 11 | before('Initialises authenticated with a default user', () => { 12 | cy.InitAuthenticatedUser() 13 | .then(authResponse => { 14 | const { 15 | response: { 16 | body: { user }, 17 | }, 18 | status, 19 | } = authResponse 20 | if (status === 200) { 21 | Object.assign(initialState.auth, { isAuthenticated: true, user }) 22 | } 23 | }) 24 | .visit('/#/') 25 | }) 26 | 27 | it('Should have attributes on initialisation', () => { 28 | const state = 'state' 29 | getStore() 30 | .its(state) 31 | .should('have.keys', Object.keys(initialState)) 32 | }) 33 | 34 | it('Attribute {auth} should have an intitial configuration', () => { 35 | const attribute = 'auth' 36 | const state = `state.${attribute}` 37 | getStore() 38 | .its(state) 39 | .should('deep.equal', initialState[attribute]) 40 | }) 41 | 42 | it('Attribute {articles} should have the vuex crud intitial configuration', () => { 43 | const attribute = 'articles' 44 | const state = `state.${attribute}` 45 | getStore() 46 | .its(state) 47 | .should('deep.equal', initialState[attribute]) 48 | }) 49 | 50 | it('Attribute {magazines} should have the vuex crud initial configuration', () => { 51 | const attribute = 'magazines' 52 | const state = `state.${attribute}` 53 | getStore() 54 | .its(state) 55 | .should('deep.equal', initialState[attribute]) 56 | }) 57 | 58 | it('Attribute {entities} should be empty', () => { 59 | const attribute = 'entities' 60 | const state = `state.${attribute}` 61 | getStore() 62 | .its(state) 63 | .should('eql', initialState[attribute]) 64 | getStore() 65 | .its(state) 66 | .should('be.empty') 67 | }) 68 | 69 | it('Attribute {resources} should have attributes initialised', () => { 70 | const attribute = 'resources' 71 | const state = `state.${attribute}` 72 | getStore() 73 | .its(state) 74 | .should('have.keys', ['routes']) 75 | }) 76 | 77 | it('Attribute {resources} should have routes initialised', () => { 78 | const attribute = 'resources' 79 | const state = `state.${attribute}` 80 | getStore() 81 | .its(state) 82 | .should('deep.equal', initialState[attribute]) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /tests/e2e/specs/ui/ui.spec.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | const { queryElementByProp } = require('../../helpers') 4 | 5 | const UI_CONTENT = require('../../../../src/constants/ui.content.default') 6 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 7 | 8 | describe('UI Test', () => { 9 | before('Initialises authenticated with a default user', () => { 10 | cy.InitAuthenticatedUser() 11 | }) 12 | 13 | it('Visits the app root url', () => { 14 | cy.visit('/#/') 15 | }) 16 | 17 | it('Title should be vue-admin', () => { 18 | cy.title().should('eq', UI_CONTENT.MAIN_TITLE) 19 | }) 20 | 21 | it('Toolbar title should be Vue Admin', () => { 22 | const mainToolbarTitleName = UI_NAMES.MAIN_TOOLBAR_TITLE 23 | const mainToolbarTitleElement = queryElementByProp({ 24 | type: 'div', 25 | prop: 'name', 26 | value: mainToolbarTitleName, 27 | }) 28 | 29 | const expectedMainToolbarTitleText = UI_CONTENT.MAIN_TOOLBAR_TITLE 30 | 31 | cy.get(mainToolbarTitleElement).should(mainToolbarTitle => { 32 | expect(mainToolbarTitle).to.contain(expectedMainToolbarTitleText) 33 | }) 34 | }) 35 | 36 | it('Toolbar hamburger button should open drawer on click', () => { 37 | const drawerButtonName = UI_NAMES.DRAWER_BUTTON 38 | 39 | const drawerButton = queryElementByProp({ 40 | type: 'button', 41 | prop: 'name', 42 | value: drawerButtonName, 43 | }) 44 | cy.get(drawerButton).click() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/e2e/specs/unauthorized/unauthorized.spec.js: -------------------------------------------------------------------------------- 1 | const Factory = require('../../factory') 2 | const UI_CONTENT = require('../../../../src/constants/ui.content.default') 3 | const UI_NAMES = require('../../../../src/constants/ui.element.names') 4 | 5 | describe('Unauthorized User Test', () => { 6 | const testResourceName = 'articles' 7 | const unauthorizedPath = 'unauthorized' 8 | const userWithoutPermissions = Factory.createUser({ permissions: [] }) 9 | const userWithPermissions = Factory.createUser({ permissions: ['admin'] }) 10 | const createAuthResponse = user => Factory.createAuthResponse({ user }) 11 | const authResponseWithUserWithoutPermissions = createAuthResponse( 12 | userWithoutPermissions 13 | ) 14 | const authResponseWithUserWithPermissions = createAuthResponse( 15 | userWithPermissions 16 | ) 17 | 18 | it('A user without admin privileges should be redirected when trying to access an unauthorized view', () => { 19 | cy.InitAuthenticatedUser({ 20 | authResponse: authResponseWithUserWithoutPermissions, 21 | }) 22 | cy.visit(`/#/${testResourceName}/`) 23 | cy.url().should('include', `/#/${unauthorizedPath}`) 24 | }) 25 | 26 | it('A user with admin privileges should not be redirected when trying to access an unauthorized view', () => { 27 | cy.InitAuthenticatedUser({ 28 | authResponse: authResponseWithUserWithPermissions, 29 | }) 30 | cy.visit(`/#/${testResourceName}/`) 31 | cy.url().should('include', `/#/${testResourceName}`) 32 | }) 33 | 34 | it('Unauthorized view should have a title', () => { 35 | const defaultHeader = UI_CONTENT.UNAUTHORIZED_HEADER 36 | cy.InitAuthenticatedUser({ 37 | authResponse: authResponseWithUserWithoutPermissions, 38 | }) 39 | cy.visit(`/#/${testResourceName}/`) 40 | cy.getElement({ 41 | constant: UI_NAMES.UNAUTHORIZED_HEADER_CONTAINER, 42 | elementType: 'h2', 43 | elementProp: 'name', 44 | }).should('contain', defaultHeader) 45 | }) 46 | 47 | it('Unauthorized view should have a text', () => { 48 | const defaultMessage = UI_CONTENT.UNAUTHORIZED_MESSAGE 49 | cy.InitAuthenticatedUser({ 50 | authResponse: authResponseWithUserWithoutPermissions, 51 | }) 52 | cy.visit(`/#/${testResourceName}/`) 53 | cy.getElement({ 54 | constant: UI_NAMES.UNAUTHORIZED_MESSAGE_CONTAINER, 55 | elementType: 'p', 56 | elementProp: 'name', 57 | }).should('contain', defaultMessage) 58 | }) 59 | 60 | it('Unauthorized view should have a button', () => { 61 | const defaultMessage = UI_CONTENT.BUTTON_GO_BACK 62 | cy.InitAuthenticatedUser({ 63 | authResponse: authResponseWithUserWithoutPermissions, 64 | }) 65 | cy.visit(`/#/${testResourceName}/`) 66 | cy.getElement({ 67 | constant: UI_NAMES.BUTTON_GO_BACK, 68 | elementType: 'button', 69 | elementProp: 'name', 70 | }).should('contain', defaultMessage) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | import { 2 | authenticate, 3 | InitAuthenticatedUser, 4 | InitEntityUtils, 5 | getElement, 6 | getStore, 7 | } from '../lib/commands' 8 | import InitServer from '../lib/server' 9 | 10 | Cypress.Commands.add('authenticate', args => authenticate(args)) 11 | Cypress.Commands.add('getStore', () => getStore()) 12 | Cypress.Commands.add('InitAuthenticatedUser', args => 13 | InitAuthenticatedUser(args) 14 | ) 15 | Cypress.Commands.add('InitEntityUtils', args => InitEntityUtils(args)) 16 | Cypress.Commands.add('getElement', args => getElement(args)) 17 | Cypress.Commands.add('InitServer', args => InitServer(args)) 18 | 19 | // *********************************************** 20 | // This example commands.js shows you how to 21 | // create various custom commands and overwrite 22 | // existing commands. 23 | // 24 | // For more comprehensive examples of custom 25 | // commands please read more here: 26 | // https://on.cypress.io/custom-commands 27 | // *********************************************** 28 | // 29 | // 30 | // -- This is a parent command -- 31 | // Cypress.Commands.add("login", (email, password) => { ... }) 32 | // 33 | // 34 | // -- This is a child command -- 35 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 36 | // 37 | // 38 | // -- This is a dual command -- 39 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 40 | // 41 | // 42 | // -- This is will overwrite an existing command -- 43 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 44 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import './commands' 17 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/factory/admin/index.js: -------------------------------------------------------------------------------- 1 | export const createAuthProvider = () => { 2 | return type => { 3 | switch (type) { 4 | default: 5 | return Promise.resolve() 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/unit/factory/auth/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates fake credentials for an Auth component 3 | * 4 | * @return {Object} An object with fake credentials 5 | */ 6 | export const createCredentials = args => { 7 | const _args = { 8 | username: 'dev@camba.coop', 9 | password: '123456', 10 | } 11 | return Object.assign({}, _args, args) 12 | } 13 | 14 | /** 15 | * Creates fake credentials for an Auth component 16 | * 17 | * @return {Object} An object with fake credentials 18 | */ 19 | export const createUser = args => { 20 | const _args = { 21 | username: 'dev@camba.coop', 22 | id: '234567', 23 | permissions: ['admin'], 24 | } 25 | return Object.assign({}, _args, args) 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/factory/index.js: -------------------------------------------------------------------------------- 1 | import { createCredentials, createUser } from './auth' 2 | import createStoreWith from './store' 3 | import { createCrudModule } from './store/modules' 4 | import { createAuthProvider } from './admin' 5 | import resource from './resource' 6 | 7 | export default { 8 | createAuthProvider, 9 | createCredentials, 10 | createCrudModule, 11 | createStoreWith, 12 | createUser, 13 | resource, 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/factory/resource/index.js: -------------------------------------------------------------------------------- 1 | import responses from './responses' 2 | 3 | export default { 4 | responses, 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/factory/resource/responses.js: -------------------------------------------------------------------------------- 1 | const getSingle = () => { 2 | return { 3 | status: 200, 4 | statusText: 'OK', 5 | headers: { 6 | 'content-length': '942', 7 | 'content-type': 'application/json; charset=utf-8', 8 | }, 9 | data: { 10 | id: 80, 11 | title: 'Hog Ma.', 12 | content: 'Im Gschicht dahoam See Leit des Freibia ewig hera, Hob auffi', 13 | }, 14 | } 15 | } 16 | 17 | export default { 18 | getSingle, 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/factory/store/common.utils.js: -------------------------------------------------------------------------------- 1 | export const initialResourcesRoutes = resources => { 2 | return resources.map(resource => { 3 | return { 4 | path: `/${resource}`, 5 | name: resource, 6 | } 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /tests/unit/factory/store/index.js: -------------------------------------------------------------------------------- 1 | import createInitialStateWith from './initial.state' 2 | import createInitialGettersWith from './initial.getters' 3 | import createInitialMutationsWith from './initial.mutations' 4 | 5 | // Initial vuex crud resources should be added here 6 | const _initialResources = ['articles', 'magazines'] 7 | 8 | /** 9 | * Annonymous Function - Creates a simualtion of initial vuex crud store 10 | * 11 | * @param {String} snapshot The name of a component the store should 12 | * be initialised for 13 | * @param {Array} initialResources A list of resources to initialise the store 14 | * 15 | * @return {type} The expected Vuex Crud mocked store for a snapshot 16 | */ 17 | export default ({ 18 | snapshot = 'default', 19 | initialResources = _initialResources, 20 | }) => { 21 | return { 22 | state: createInitialStateWith({ snapshot, initialResources }), 23 | getters: createInitialGettersWith({ snapshot, initialResources }), 24 | mutations: createInitialMutationsWith({ snapshot }), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/unit/factory/store/initial.getters.js: -------------------------------------------------------------------------------- 1 | import { initialResourcesRoutes } from './common.utils' 2 | import { Types as EntitiesTypes } from '@store/modules/resources' 3 | import { Types as ResourcesTypes } from '@store/modules/resources' 4 | 5 | /** 6 | * Annonymous Function - Creates a simualtion of initial vuex crud getters 7 | * 8 | * @param {String} snapshot The name of a component the getters should 9 | * be initialised for 10 | * @param {Array} initialResources A list of resources to initialise getters 11 | * 12 | * @return {Object} The expected Vuex Crud mocked getters 13 | */ 14 | export default ({ snapshot = 'default', initialResources }) => { 15 | // New custom getters configurations should be added here 16 | const snapshots = { 17 | default: initDefaultGetters, 18 | Resource: initGettersForResource, 19 | } 20 | 21 | // Vuex initial entities getters should be added here 22 | const { namespace: entitiesNamespace, ENTITIES_GET_ENTITY } = EntitiesTypes 23 | const entitiesCrud = { 24 | [`${entitiesNamespace}/${ENTITIES_GET_ENTITY}`]: () => {}, 25 | } 26 | // Vuex initial resources getters should be added here 27 | const { 28 | namespace: resourcesNamespace, 29 | RESOURCES_GET_ALL_ROUTES, 30 | } = ResourcesTypes 31 | const resourcesGetters = { 32 | [`${resourcesNamespace}/${RESOURCES_GET_ALL_ROUTES}`]: () => { 33 | return initialResourcesRoutes(initialResources) 34 | }, 35 | } 36 | 37 | /** 38 | * initResourcesCrud - Given a list of resources, creates mocked vuex crud 39 | * methods for each of them 40 | * 41 | * @param {Array} resources An array of strings 42 | * 43 | * @return {Object} An object with mocked vuex crud methods 44 | */ 45 | function initResourcesGetters(resources) { 46 | const crud = {} 47 | resources.forEach(resource => { 48 | ;(crud[`${resource}/byId`] = id => id), 49 | (crud[`${resource}/isError`] = () => false), 50 | (crud[`${resource}/isLoading`] = () => false), 51 | (crud[`${resource}/list`] = () => []) 52 | }) 53 | return crud 54 | } 55 | // Initialises default getters 56 | function initDefaultGetters() { 57 | return { 58 | ...initResourcesGetters(initialResources), 59 | ...entitiesCrud, 60 | ...resourcesGetters, 61 | } 62 | } 63 | // Initialises getters for a Resource component 64 | function initGettersForResource() { 65 | return { 66 | ...initResourcesGetters(initialResources), 67 | } 68 | } 69 | 70 | return snapshots[snapshot]() 71 | } 72 | -------------------------------------------------------------------------------- /tests/unit/factory/store/initial.mutations.js: -------------------------------------------------------------------------------- 1 | import { Types as ResourcesTypes } from '@store/modules/resources' 2 | 3 | /** 4 | * Annonymous Function - Creates a simualtion of initial vuex crud mutations 5 | * 6 | * @param {String} snapshot The name of a component the mutations should be 7 | * initialised for 8 | * 9 | * @return {Object} The expected Vuex Crud mocked mutations 10 | */ 11 | export default ({ snapshot = 'default' }) => { 12 | // New custom mutations configurations should be added here 13 | const snapshots = { 14 | default: initDefaultMutations, 15 | Resource: initMutationsForResource, 16 | } 17 | 18 | // Vuex initial resources mutations should be added here 19 | const { namespace: resourcesNamespace, RESOURCES_ADD_ROUTE } = ResourcesTypes 20 | const resourcesMutations = { 21 | [`${resourcesNamespace}/${RESOURCES_ADD_ROUTE}`]: (state, args) => { 22 | args.addedRouteCallback && args.addedRouteCallback() 23 | }, 24 | } 25 | 26 | // Initialises default mutations 27 | function initDefaultMutations() { 28 | return { 29 | ...resourcesMutations, 30 | } 31 | } 32 | // Initialises mutations for a Resource component 33 | function initMutationsForResource() { 34 | return { 35 | ...resourcesMutations, 36 | } 37 | } 38 | 39 | return snapshots[snapshot]() 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/factory/store/initial.state.js: -------------------------------------------------------------------------------- 1 | import { initialResourcesRoutes } from './common.utils' 2 | 3 | /** 4 | * Annonymous Function - Creates a simualtion of initial vuex crud state 5 | * 6 | * @param {String} snapshot The name of a component the state should be 7 | * initialised for 8 | * @param {Array} initialResources A list of resources to initialise the state 9 | * 10 | * @return {Object} The expected Vuex Crud mocked state 11 | */ 12 | export default ({ snapshot = 'default', initialResources }) => { 13 | // New custom mutations configurations should be added here 14 | const snapshots = { 15 | default: initDefaultState, 16 | Resource: initStateForResource, 17 | } 18 | // Vuex Crud Initial State for a resource 19 | const initialResourceState = { 20 | createError: null, 21 | destroyError: null, 22 | entities: {}, 23 | fetchListError: null, 24 | fetchSingleError: null, 25 | isCreating: false, 26 | isDestroying: false, 27 | isFetchingList: false, 28 | isFetchingSingle: false, 29 | isReplacing: false, 30 | isUpdating: false, 31 | list: [], 32 | replaceError: null, 33 | updateError: null, 34 | } 35 | // Vuex Initial State for entities 36 | const initialEntitiesState = {} 37 | // Vuex Initial State for resource routes 38 | const initialResourcesState = { 39 | routes: initialResourcesRoutes(initialResources), 40 | } 41 | 42 | /** 43 | * initResourcesCrud - Given a list of resources, creates mocked vuex crud 44 | * state for each of them 45 | * 46 | * @param {Array} resources An array of strings 47 | * 48 | * @return {Object} An object with mocked vuex crud state 49 | */ 50 | function initResourcesState(resources) { 51 | const _resources = {} 52 | resources.forEach(resource => { 53 | _resources[resource] = initialResourceState 54 | }) 55 | return _resources 56 | } 57 | // Initialises default state 58 | function initDefaultState() { 59 | return { 60 | ...initResourcesState(initialResources), 61 | entities: initialEntitiesState, 62 | resources: initialResourcesState, 63 | } 64 | } 65 | // Initialises state for a Resource component 66 | function initStateForResource() { 67 | return {} 68 | } 69 | 70 | snapshots[snapshot]() 71 | } 72 | -------------------------------------------------------------------------------- /tests/unit/factory/store/modules/index.js: -------------------------------------------------------------------------------- 1 | import { createCrudModule as _createCrudModule } from '@store/modules' 2 | import defaults from '@components/Resource/src/defaults' 3 | 4 | export const createCrudModule = args => { 5 | const apiUrl = 'localhost/api/' 6 | return _createCrudModule({ 7 | apiUrl, 8 | resourceName: 'resource', 9 | resourceIdName: 'id', 10 | parseResponses: defaults().props.parseResponses, 11 | ...args, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/fixtures/admin/index.js: -------------------------------------------------------------------------------- 1 | import { AppLayout, AuthLayout, UnauthorizedLayout } from '@components/Layouts' 2 | import Core from '@components/Core' 3 | import defaults, { 4 | authenticatedDefaults, 5 | unauthenticatedDefaults, 6 | } from '@components/Admin/src/defaults' 7 | import { SimpleSidebar } from '@components/UiComponents' 8 | 9 | export default { 10 | props: { 11 | ...defaults().props, 12 | }, 13 | args: { 14 | ...defaults().args, 15 | }, 16 | } 17 | 18 | export const Authenticated = { 19 | props: { 20 | layout: AppLayout, 21 | sidebar: SimpleSidebar, 22 | title: 'A toolbar text', 23 | unauthorized: UnauthorizedLayout, 24 | }, 25 | args: { 26 | Core, 27 | ...authenticatedDefaults.args, 28 | }, 29 | } 30 | 31 | export const Unauthenticated = { 32 | props: { 33 | layout: AuthLayout, 34 | }, 35 | args: { 36 | ...unauthenticatedDefaults.args, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/fixtures/auth/index.js: -------------------------------------------------------------------------------- 1 | import defaults from '@components/Layouts/src/AuthLayout/defaults' 2 | 3 | export default state => { 4 | const { username: userCredential, password: passwordCredential } = state 5 | 6 | return { 7 | props: { 8 | authFormTitle: defaults().props.authFormTitle, 9 | authFooter: defaults().props.authFooter, 10 | authMainContent: defaults().props.authMainContent, 11 | usernameRules: defaults().props.usernameRules, 12 | passwordRules: defaults().props.passwordRules, 13 | va: { 14 | login: (username, password) => { 15 | return username === userCredential && password === passwordCredential 16 | ? { then: () => {} } 17 | : new Promise(resolve => { 18 | resolve({ 19 | response: { 20 | status: 401, 21 | }, 22 | }) 23 | }) 24 | }, 25 | }, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/fixtures/resource/magazines.js: -------------------------------------------------------------------------------- 1 | import CreateMagazines from '@/../demo/components/magazines/CreateMagazines' 2 | import EditMagazines from '@/../demo/components/magazines/EditMagazines' 3 | import ListMagazines from '@/../demo/components/magazines/ListMagazines' 4 | import ShowMagazines from '@/../demo/components/magazines/ShowMagazines' 5 | import defaults from '@components/Resource/src/defaults' 6 | import { Types } from '@store/modules/resources' 7 | 8 | const { namespace, RESOURCES_ADD_ROUTE } = Types 9 | 10 | export default { 11 | props: Object.assign({}, defaults().props, { 12 | name: 'magazines', 13 | apiUrl: 'http://localhost:8888', 14 | resourceIdName: defaults().props.resourceIdName, 15 | userPermissionsField: defaults().props.userPermissionsField, 16 | create: CreateMagazines, 17 | edit: EditMagazines, 18 | list: ListMagazines, 19 | show: ShowMagazines, 20 | redirect: defaults().props.redirect(), 21 | parseResponses: defaults().props.parseResponses(), 22 | }), 23 | methods: { 24 | storeMethods: { 25 | [`${namespace}/${RESOURCES_ADD_ROUTE}`]: { 26 | params: { 27 | path: '/magazines', 28 | name: 'magazines', 29 | }, 30 | }, 31 | }, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/fixtures/ui-components/date.input.js: -------------------------------------------------------------------------------- 1 | import defaults from '@components/UiComponents/DateField/src/defaults' 2 | 3 | export default { 4 | props: { 5 | disabled: defaults().props.disabled, 6 | format: () => {}, 7 | name: defaults().props.name, 8 | parse: () => {}, 9 | placeholder: 'select a date', 10 | readonly: defaults().props.readonly, 11 | vDatePickerProps: defaults().props.vDatePickerProps(), 12 | vMenuProps: defaults().props.vMenuProps(), 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/lib/constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | VUETIFY_TEXT_FIELD_LABEL_DETAILS_CLASS: '.v-messages__message', 3 | } 4 | -------------------------------------------------------------------------------- /tests/unit/lib/utils/wrapper.js: -------------------------------------------------------------------------------- 1 | const nextTick = wrapper => { 2 | return new Promise(resolve => wrapper.vm.$nextTick(resolve)) 3 | } 4 | 5 | const findElemByName = ({ wrapper, el, name, prop = 'name' }) => { 6 | return wrapper.find(`${el}[${prop}="${name}"]`) 7 | } 8 | 9 | const findRef = ({ wrapper, ref }) => wrapper.find({ ref }) 10 | 11 | export { findElemByName, findRef, nextTick } 12 | -------------------------------------------------------------------------------- /tests/unit/specs/components/admin/unauthenticated.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Vuex from 'vuex' 4 | import Unauthenticated from '@components/Admin/src/Unauthenticated' 5 | import Factory from '@unit/factory' 6 | import AuthTypes from '@va-auth/types' 7 | import authStore from '@va-auth/store' 8 | import { shallowMount } from '@vue/test-utils' 9 | import { Unauthenticated as unauthenticatedFixture } from '@unit/fixtures/admin' 10 | 11 | describe('Unauthenticated.vue', () => { 12 | const subject = 'Unauthenticated' 13 | 14 | Vue.use(Vuex) 15 | 16 | // subject 17 | let subjectWrapper 18 | // mocks 19 | let mockedRouter 20 | let mockedStore 21 | let mocks 22 | // spies 23 | let storeSpy 24 | // props 25 | let propsData 26 | 27 | function mountSubject() { 28 | subjectWrapper = shallowMount(Unauthenticated, { 29 | mocks, 30 | propsData, 31 | router: mockedRouter, 32 | sync: true, 33 | }) 34 | } 35 | 36 | beforeEach(() => { 37 | const routes = [{}] 38 | mockedRouter = new VueRouter(routes) 39 | mockedStore = new Vuex.Store({ 40 | modules: { 41 | auth: authStore({ client: () => new Promise(() => {}) }), 42 | }, 43 | }) 44 | mocks = { $store: mockedStore, $router: mockedRouter } 45 | propsData = { 46 | layout: unauthenticatedFixture.props.layout, 47 | } 48 | storeSpy = { 49 | dispatch: jest.spyOn(mocks.$store, 'dispatch'), 50 | } 51 | }) 52 | 53 | afterEach(() => { 54 | subjectWrapper = {} 55 | }) 56 | 57 | it('should have props', () => { 58 | mountSubject() 59 | 60 | const props = subjectWrapper.props() 61 | 62 | expect(subjectWrapper.name()).toMatch(subject) 63 | expect(props.layout).toMatchObject(unauthenticatedFixture.props.layout) 64 | }) 65 | 66 | it('[Auth] - component is rendered', async () => { 67 | mountSubject() 68 | 69 | const { 70 | props: { layout }, 71 | } = unauthenticatedFixture 72 | const authComponent = subjectWrapper.find(layout) 73 | 74 | expect(authComponent.exists()).toBe(true) 75 | }) 76 | 77 | it('when the login method is called an [AUTH_LOGIN_REQUEST] action is dispatched', () => { 78 | mountSubject() 79 | 80 | const { namespace, AUTH_LOGIN_REQUEST } = AuthTypes 81 | const action = `${namespace}/${AUTH_LOGIN_REQUEST}` 82 | const { username, password } = Factory.createCredentials() 83 | 84 | subjectWrapper.vm.login(username, password) 85 | 86 | expect(storeSpy.dispatch).toHaveBeenCalledTimes(1) 87 | expect(storeSpy.dispatch).toHaveBeenCalledWith(action, { 88 | username, 89 | password, 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/unit/specs/layouts/applayout.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import Vuex from 'vuex' 4 | import AppLayout from '@components/Layouts/src/AppLayout' 5 | import SimpleSidebar from '@components/UiComponents/Sidebar/SimpleSidebar' 6 | import AuthTypes from '@va-auth/types' 7 | import { mount } from '@vue/test-utils' 8 | 9 | describe('AppLayout', () => { 10 | Vue.use(Vuetify) 11 | Vue.use(Vuex) 12 | 13 | let subjectWrapper 14 | // stubs 15 | let propsData 16 | let vuetify 17 | // mocks 18 | let mocks 19 | let mockedStore 20 | 21 | // Mounts the component 22 | function mountSubject() { 23 | subjectWrapper = mount(AppLayout, { 24 | mocks, 25 | propsData, 26 | vuetify, 27 | }) 28 | } 29 | 30 | beforeEach(() => { 31 | AppLayout.install(Vue) 32 | // Configures the subject props 33 | propsData = { 34 | sidebar: SimpleSidebar, 35 | title: 'My AppLayout title', 36 | va: { 37 | getUser: () => { 38 | const { namespace, AUTH_GET_USER } = AuthTypes 39 | return this.$store.getters[`${namespace}/${AUTH_GET_USER}`] 40 | }, 41 | logout: () => { 42 | const { namespace: authNamespace, AUTH_LOGOUT_REQUEST } = AuthTypes 43 | const actionName = `${authNamespace}/${AUTH_LOGOUT_REQUEST}` 44 | mockedStore.dispatch(actionName) 45 | }, 46 | }, 47 | } 48 | vuetify = new Vuetify() 49 | mockedStore = new Vuex.Store({}) 50 | mocks = { 51 | $store: mockedStore, 52 | } 53 | }) 54 | 55 | it('should have props', () => { 56 | mountSubject() 57 | const appLayoutProps = subjectWrapper.props() 58 | 59 | expect(appLayoutProps.sidebar).toBe(propsData.sidebar) 60 | expect(appLayoutProps.title).toMatch(propsData.title) 61 | expect(appLayoutProps.va).toBe(propsData.va) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /tests/unit/specs/layouts/homelayout.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import HomeLayout from '@components/Layouts/src/HomeLayout' 4 | import { mount } from '@vue/test-utils' 5 | 6 | describe('HomeLayout', () => { 7 | Vue.use(Vuetify) 8 | 9 | let subjectWrapper 10 | // stubs 11 | let propsData 12 | let vuetify 13 | 14 | // Mounts the component 15 | function mountSubject() { 16 | subjectWrapper = mount(HomeLayout, { 17 | propsData, 18 | vuetify, 19 | }) 20 | } 21 | 22 | beforeEach(() => { 23 | HomeLayout.install(Vue) 24 | vuetify = new Vuetify() 25 | }) 26 | 27 | it('should have props', () => { 28 | mountSubject() 29 | 30 | expect(subjectWrapper.text()).toMatch('Welcome to Vue-Admin') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/unit/specs/ui-components/date.input.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import { mount } from '@vue/test-utils' 4 | import DateField from '@components/UiComponents/DateField' 5 | import ERROR_MESSAGES from '@constants/error.messages' 6 | import dateInputFixture from '@unit/fixtures/ui-components/date.input.js' 7 | 8 | describe('DateField.vue', () => { 9 | const subject = 'DateField' 10 | // Initialises the vue instance and DateField dependencies 11 | Vue.use(Vuetify) 12 | 13 | let vuetify 14 | let subjectWrapper 15 | let propsData 16 | 17 | beforeEach(() => { 18 | // Configures the subject props 19 | propsData = { 20 | ...dateInputFixture.props, 21 | } 22 | vuetify = new Vuetify() 23 | }) 24 | 25 | it('should have default props', () => { 26 | // Exercise: mounts the subject instance 27 | mountSubject() 28 | const props = subjectWrapper.props() 29 | expect(subjectWrapper.name()).toMatch(subject) 30 | expect(props.disabled).toBe(false) 31 | expect(props.format).toBeDefined() 32 | expect(props.name).toMatch(dateInputFixture.props.name) 33 | expect(props.parse).toBeDefined() 34 | expect(props.readonly).toBe(true) 35 | expect(props.vDatePickerProps).toMatchObject( 36 | dateInputFixture.props.vDatePickerProps 37 | ) 38 | expect(props.vMenuProps).toMatchObject(dateInputFixture.props.vMenuProps) 39 | }) 40 | 41 | it('should have non default props', () => { 42 | mountSubject() 43 | const props = subjectWrapper.props() 44 | expect(props.placeholder).toBe(dateInputFixture.props.placeholder) 45 | }) 46 | 47 | it('throws Error when the {format} property is missing', () => { 48 | const prop = 'format' 49 | shouldThrowOnMissingProp({ prop, subject }) 50 | }) 51 | 52 | it('throws Error when the {parse} proprty is missing', () => { 53 | const prop = 'parse' 54 | shouldThrowOnMissingProp({ prop, subject }) 55 | }) 56 | 57 | it('throws Error when the {valid} property is missing', () => { 58 | const prop = 'valid' 59 | shouldThrowOnMissingProp({ prop, subject }) 60 | }) 61 | 62 | /** 63 | * Helper functions 64 | */ 65 | 66 | function shouldThrowOnMissingProp({ prop, subject }) { 67 | // Setup: deletes the list prop before mounting 68 | delete propsData[prop] 69 | const { UNDEFINED_PROPERTY } = ERROR_MESSAGES 70 | const at = subject 71 | const message = UNDEFINED_PROPERTY.with({ prop, at }) 72 | const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {}) 73 | 74 | try { 75 | // Exercise: mounts the subject instance 76 | mountSubject() 77 | } catch (error) { 78 | expect(error.message).toBe(message) 79 | } finally { 80 | spy.mockRestore() 81 | } 82 | } 83 | 84 | // Mounts the component 85 | function mountSubject() { 86 | subjectWrapper = mount(DateField, { 87 | propsData, 88 | vuetify, 89 | }) 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /tests/unit/specs/ui-components/simple.text.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import { SimpleText } from '@components/UiComponents' 4 | import { defaults } from '@components/UiComponents/SimpleText/src/SimpleText' 5 | import Factory from '@unit/factory' 6 | import { shallowMount } from '@vue/test-utils' 7 | 8 | describe('SimpleText.vue', () => { 9 | Vue.use(Vuetify) 10 | const vuetify = new Vuetify() 11 | 12 | let defaultProps 13 | let propsData 14 | let subjectWrapper 15 | 16 | function mountSubject() { 17 | subjectWrapper = shallowMount(SimpleText, { 18 | propsData, 19 | vuetify, 20 | }) 21 | } 22 | 23 | beforeEach(() => { 24 | SimpleText.install(Vue) 25 | defaultProps = defaults().props 26 | 27 | propsData = { 28 | parse: value => value, 29 | type: 'p', 30 | value: 'Im an empty content', 31 | } 32 | }) 33 | 34 | afterEach(() => { 35 | subjectWrapper = {} 36 | }) 37 | 38 | it('should have props', () => { 39 | mountSubject() 40 | 41 | const props = subjectWrapper.props() 42 | 43 | expect(props.type).toMatch(propsData.type) 44 | expect(props.value).toMatch(propsData.value) 45 | }) 46 | 47 | it('should use default props when none were provided', () => { 48 | delete propsData.type 49 | delete propsData.parse 50 | delete propsData.value 51 | mountSubject() 52 | 53 | // Asserts to the post-mounting generated props by default 54 | const { parse, type, value } = defaultProps 55 | 56 | const response = Factory.resource.responses.getSingle() 57 | 58 | expect(subjectWrapper.vm._props.parse(response)).toMatchObject( 59 | parse(response) 60 | ) 61 | expect(subjectWrapper.vm._props.type).toMatch(type) 62 | expect(subjectWrapper.vm._props.value).toMatch(value) 63 | }) 64 | 65 | it('should render a parsed content', () => { 66 | mountSubject() 67 | 68 | const value = subjectWrapper.vm.parse(propsData.value) 69 | 70 | expect(subjectWrapper.text()).toMatch(value) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/unit/specs/ui-components/spinner.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import { Spinner } from '@components/UiComponents' 4 | import { defaults } from '@components/UiComponents/Spinner/Spinner' 5 | import { findElemByName } from '@unit/lib/utils/wrapper' 6 | import { shallowMount } from '@vue/test-utils' 7 | 8 | describe('Spinner.vue', () => { 9 | Vue.use(Vuetify) 10 | const vuetify = new Vuetify() 11 | 12 | let defaultProps 13 | let propsData 14 | let subjectWrapper 15 | 16 | function mountSubject() { 17 | subjectWrapper = shallowMount(Spinner, { 18 | propsData, 19 | vuetify, 20 | }) 21 | } 22 | 23 | function findSpinnerContainer({ name }) { 24 | return findElemByName({ 25 | wrapper: subjectWrapper, 26 | el: 'v-container-stub', 27 | name: name, 28 | prop: 'id', 29 | }) 30 | } 31 | 32 | beforeEach(() => { 33 | Spinner.install(Vue) 34 | defaultProps = defaults().props 35 | 36 | propsData = { 37 | isLoading: true, 38 | name: 'a-custom-name', 39 | vProps: { 40 | color: 'warning', 41 | indeterminate: true, 42 | }, 43 | } 44 | }) 45 | 46 | afterEach(() => { 47 | subjectWrapper = {} 48 | }) 49 | 50 | it('should have props', () => { 51 | mountSubject() 52 | 53 | const { isLoading, name, vProps } = propsData 54 | const props = subjectWrapper.props() 55 | 56 | expect(props.isLoading).toBe(isLoading) 57 | expect(props.name).toMatch(name) 58 | expect(props.vProps).toMatchObject(vProps) 59 | }) 60 | 61 | it('should have default props', () => { 62 | delete propsData.isLoading 63 | delete propsData.name 64 | delete propsData.vProps 65 | mountSubject() 66 | 67 | const { isLoading, name, vProps } = defaultProps 68 | const props = subjectWrapper.props() 69 | 70 | expect(props.isLoading).toBe(isLoading) 71 | expect(props.name).toMatch(name) 72 | expect(props.vProps).toMatchObject(vProps) 73 | }) 74 | 75 | it('should exists when isLoading is true', () => { 76 | mountSubject() 77 | subjectWrapper.setProps({ isLoading: true }) 78 | 79 | const container = findSpinnerContainer({ name: propsData.name }) 80 | 81 | expect(container.exists()).toBe(true) 82 | }) 83 | 84 | it('should not exist when isLoading is false', () => { 85 | mountSubject() 86 | subjectWrapper.setProps({ isLoading: false }) 87 | 88 | const container = findSpinnerContainer({ name: defaultProps.name }) 89 | 90 | expect(container.exists()).toBe(false) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/unit/specs/ui-components/text.field.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import { TextField } from '@components/UiComponents' 4 | import { defaults } from '@components/UiComponents/TextField/src/TextField' 5 | import { findElemByName } from '@unit/lib/utils/wrapper' 6 | import { shallowMount } from '@vue/test-utils' 7 | 8 | describe('TextField.vue', () => { 9 | Vue.use(Vuetify) 10 | const vuetify = new Vuetify() 11 | 12 | let defaultProps 13 | let propsData 14 | let subjectWrapper 15 | 16 | function mountSubject() { 17 | subjectWrapper = shallowMount(TextField, { 18 | propsData, 19 | vuetify, 20 | }) 21 | } 22 | 23 | beforeEach(() => { 24 | TextField.install(Vue) 25 | defaultProps = defaults().props 26 | 27 | propsData = { 28 | name: 'my-text-field-name', 29 | placeHolder: 'write your content here', 30 | value: 'Im an empty content', 31 | } 32 | }) 33 | 34 | afterEach(() => { 35 | subjectWrapper = {} 36 | }) 37 | 38 | it('should have props', () => { 39 | mountSubject() 40 | 41 | const props = subjectWrapper.props() 42 | 43 | expect(props.name).toMatch(propsData.name) 44 | expect(props.placeHolder).toMatch(propsData.placeHolder) 45 | expect(props.value).toMatch(propsData.value) 46 | }) 47 | 48 | it('should use default props when none were provided', () => { 49 | delete propsData.name 50 | delete propsData.placeHolder 51 | delete propsData.value 52 | mountSubject() 53 | 54 | // Asserts to the post-mounting generated props by default 55 | const { name, placeHolder, value } = defaultProps 56 | 57 | expect(subjectWrapper.vm._props.name).toMatch(name) 58 | expect(subjectWrapper.vm._props.placeHolder).toMatch(placeHolder) 59 | expect(subjectWrapper.vm._props.value).toMatch(value) 60 | }) 61 | 62 | it('should render a parsed content', () => { 63 | mountSubject() 64 | 65 | const textField = findElemByName({ 66 | wrapper: subjectWrapper, 67 | el: 'v-text-field-stub', 68 | name: propsData.name, 69 | }) 70 | const textFieldSpy = { 71 | emit: jest.spyOn(textField.vm, '$emit'), 72 | } 73 | 74 | subjectWrapper.vm.$nextTick() 75 | 76 | const newValue = 'a new val' 77 | textField.vm.inputValue = newValue 78 | 79 | expect(textFieldSpy.emit).toHaveBeenCalledTimes(1) 80 | expect(textFieldSpy.emit).toHaveBeenCalledWith('change', newValue) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /utils/server-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "dependencies": { 11 | "bavaria-ipsum": "^1.0.3", 12 | "bcrypt": "^5.0.0", 13 | "body-parser": "^1.18.2", 14 | "cors": "^2.8.4", 15 | "express": "^4.16.2", 16 | "fake-data-generator": "^0.1.10", 17 | "jsonwebtoken": "^8.5.0" 18 | }, 19 | "author": "", 20 | "license": "ISC" 21 | } 22 | -------------------------------------------------------------------------------- /utils/server-test/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const express = require('express') 3 | const bodyParser = require('body-parser') 4 | 5 | const createMagazinesService = require('./services/magazines') 6 | const createArticlesService = require('./services/articles') 7 | const createAuthorsService = require('./services/authors') 8 | const createAuthService = require('./services/auth') 9 | 10 | /* eslint-enable */ 11 | 12 | var cors = require('cors') 13 | const app = express() 14 | 15 | app.use(cors()) 16 | app.options('*', cors()) 17 | 18 | app.use(express.static(__dirname)) 19 | 20 | app.use(bodyParser.json()) 21 | 22 | createArticlesService(app) 23 | createMagazinesService(app) 24 | createAuthService(app) 25 | createAuthorsService(app) 26 | 27 | const port = process.env.PORT || 8080 28 | 29 | module.exports = app.listen(port, () => { 30 | /* eslint-disable no-console */ 31 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 32 | /* eslint-enable no-console */ 33 | }) 34 | -------------------------------------------------------------------------------- /utils/server-test/services/articles.js: -------------------------------------------------------------------------------- 1 | const Ipsum = require('bavaria-ipsum') 2 | 3 | module.exports = function(app) { 4 | const ipsum = new Ipsum() 5 | 6 | const articles = [ 7 | { 8 | id: 1, 9 | title: ipsum.generateSentence(), 10 | content: ipsum.generateParagraph(), 11 | }, 12 | { 13 | id: 2, 14 | title: ipsum.generateSentence(), 15 | content: ipsum.generateParagraph(), 16 | }, 17 | { 18 | id: 3, 19 | title: ipsum.generateSentence(), 20 | content: ipsum.generateParagraph(), 21 | }, 22 | ] 23 | 24 | app.get('/api/articles', (req, res) => { 25 | res.json(articles) 26 | }) 27 | 28 | app.get('/api/articles/:id', (req, res) => { 29 | const article = articles.find(a => a.id.toString() === req.params.id) 30 | const index = articles.indexOf(article) 31 | 32 | res.json(articles[index]) 33 | }) 34 | 35 | app.patch('/api/articles/:id', (req, res) => { 36 | const { body } = req 37 | const article = articles.find(a => a.id.toString() === req.params.id) 38 | const index = articles.indexOf(article) 39 | 40 | if (index >= 0) { 41 | article.title = body.title 42 | article.content = body.content 43 | articles[index] = article 44 | } 45 | 46 | res.json(article) 47 | }) 48 | 49 | app.put('/api/articles/:id', (req, res) => { 50 | const { body } = req 51 | const article = articles.find(a => a.id.toString() === req.params.id) 52 | const index = articles.indexOf(article) 53 | 54 | if (index >= 0) { 55 | article.title = body.title 56 | article.content = body.content 57 | articles[index] = article 58 | } 59 | 60 | res.json(article) 61 | }) 62 | 63 | app.delete('/api/articles/:id', (req, res) => { 64 | const article = articles.find(a => a.id.toString() === req.params.id) 65 | const index = articles.indexOf(article) 66 | 67 | if (index >= 0) articles.splice(index, 1) 68 | 69 | res.status(202).send() 70 | }) 71 | 72 | app.post('/api/articles', (req, res) => { 73 | let id 74 | if (!articles.length) { 75 | id = 0 76 | } else { 77 | id = articles[articles.length - 1].id + 1 78 | } 79 | const { body } = req 80 | 81 | const article = { 82 | id, 83 | title: body.title, 84 | content: body.content, 85 | } 86 | 87 | articles.push(article) 88 | 89 | res.status(201).send(article) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /utils/server-test/services/auth.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const jwt = require('jsonwebtoken') 3 | 4 | const secret = 'mySecret' 5 | 6 | module.exports = function(app) { 7 | let whitelist = [ 8 | { 9 | id: 123456, 10 | email: 'user@camba.coop', 11 | password: '$2b$08$ZRrJWppetUmKe5Zlc1Mtj.w51ZTmmx2M4YKUdS4QOBHXq2gzF/zyS', // bcrypt.hashSync('123456') 12 | permissions: ['guest'], 13 | token: '', 14 | }, 15 | { 16 | id: 234567, 17 | email: 'dev@camba.coop', 18 | password: '$2b$08$ZRrJWppetUmKe5Zlc1Mtj.w51ZTmmx2M4YKUdS4QOBHXq2gzF/zyS', // bcrypt.hashSync('123456') 19 | permissions: ['admin'], 20 | token: '', 21 | }, 22 | ] 23 | 24 | app.post('/api/auth', (req, res) => { 25 | const user = whitelist.find(user => user.email === req.headers.username) 26 | if (!user) return res.status(401).send('Invalid user or password') 27 | const isPasswordValid = bcrypt.compareSync( 28 | req.headers.password, 29 | user.password 30 | ) 31 | if (!isPasswordValid) 32 | return res.status(401).send('Invalid user or password') 33 | const token = jwt.sign({ id: user.id }, secret, { expiresIn: 3600 }) 34 | // Saves the token in the whitelist user array 35 | whitelist = whitelist.map(_user => { 36 | if (_user.id === user.id) { 37 | _user.token = token 38 | return _user 39 | } 40 | return _user 41 | }) 42 | const newUser = Object.assign({}, user) 43 | delete newUser.password 44 | delete newUser.token 45 | return res.status(200).send({ auth: true, token, user: newUser }) 46 | }) 47 | 48 | app.get('/api/auth', (req, res) => { 49 | const token = req.headers.token 50 | if (!token) return res.status(401).send('Invalid token') 51 | jwt.verify(token, secret, (err, decodedToken) => { 52 | if (err) return res.status(401).send('Token is invalid or has expired') 53 | const user = {} 54 | Object.assign(user, whitelist.find(user => user.id === decodedToken.id)) 55 | delete user.token 56 | delete user.password 57 | return res.status(200).send({ user }) 58 | }) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /utils/server-test/services/authors.js: -------------------------------------------------------------------------------- 1 | const { generateModel } = require('fake-data-generator') 2 | 3 | module.exports = function(app) { 4 | const model = { 5 | config: { 6 | locale: 'en', 7 | }, 8 | model: { 9 | type: 'Object', 10 | value: { 11 | id: { 12 | type: 'randomNumberBetween', 13 | value: [1, 2500000], 14 | }, 15 | name: { 16 | type: 'faker', 17 | value: 'name.firstName', 18 | }, 19 | lastname: { 20 | type: 'faker', 21 | value: 'name.lastName', 22 | }, 23 | birthdate: { 24 | type: 'faker', 25 | value: 'date.between', 26 | options: ['1970-01-01', '1996-12-31'], 27 | }, 28 | }, 29 | }, 30 | } 31 | 32 | const authors = generateModel({ 33 | amountArg: 40, 34 | modelArg: model, 35 | inputType: 'object', 36 | outputType: 'object', 37 | }) 38 | 39 | app.get('/api/authors', (req, res) => { 40 | res.json(authors) 41 | }) 42 | 43 | app.get('/api/authors/:id', (req, res) => { 44 | const author = authors.find(a => a.id.toString() === req.params.id) 45 | const index = authors.indexOf(author) 46 | 47 | res.json(authors[index]) 48 | }) 49 | 50 | app.patch('/api/authors/:id', (req, res) => { 51 | const { body } = req 52 | const author = authors.find(a => a.id.toString() === req.params.id) 53 | const index = authors.indexOf(author) 54 | 55 | if (index >= 0) { 56 | author.name = body.name 57 | author.lastname = body.lastname 58 | author.birthdate = body.birthdate 59 | authors[index] = author 60 | } 61 | 62 | res.json(author) 63 | }) 64 | 65 | app.put('/api/authors/:id', (req, res) => { 66 | const { body } = req 67 | const author = authors.find(a => a.id.toString() === req.params.id) 68 | const index = authors.indexOf(author) 69 | 70 | if (index >= 0) { 71 | author.name = body.name 72 | author.lastname = body.lastname 73 | author.birthdate = body.birthdate 74 | authors[index] = author 75 | } 76 | 77 | res.json(author) 78 | }) 79 | 80 | app.delete('/api/authors/:id', (req, res) => { 81 | const author = authors.find(a => a.id.toString() === req.params.id) 82 | const index = authors.indexOf(author) 83 | 84 | if (index >= 0) authors.splice(index, 1) 85 | 86 | res.status(202).send() 87 | }) 88 | 89 | app.post('/api/authors', (req, res) => { 90 | let id 91 | if (!authors.length) { 92 | id = 0 93 | } else { 94 | id = authors[authors.length - 1].id + 1 95 | } 96 | const { body } = req 97 | 98 | const author = { 99 | id, 100 | name: body.name, 101 | lastname: body.lastname, 102 | birthdate: body.birthdate, 103 | } 104 | 105 | authors.push(author) 106 | 107 | res.status(201).send(author) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /utils/server-test/services/magazines.js: -------------------------------------------------------------------------------- 1 | const Ipsum = require('bavaria-ipsum') 2 | 3 | module.exports = function(app) { 4 | const ipsum = new Ipsum() 5 | 6 | const magazines = [ 7 | { 8 | id: 1, 9 | name: 'Console log Oriented Programming', 10 | articles: [1, 2, 3], 11 | issue: '#20', 12 | publisher: ipsum.generateParagraph(1), 13 | }, 14 | { 15 | id: 2, 16 | name: ipsum.generateSentence(), 17 | articles: [], 18 | issue: '#13', 19 | publisher: ipsum.generateParagraph(1), 20 | }, 21 | { 22 | id: 3, 23 | name: ipsum.generateSentence(), 24 | articles: [], 25 | issue: '#7', 26 | publisher: ipsum.generateParagraph(1), 27 | }, 28 | ] 29 | 30 | app.get('/api/magazines', (req, res) => { 31 | res.json(magazines) 32 | }) 33 | 34 | app.get('/api/magazines/:id', (req, res) => { 35 | const magazine = magazines.find(a => a.id.toString() === req.params.id) 36 | const index = magazines.indexOf(magazine) 37 | 38 | res.json(magazines[index]) 39 | }) 40 | 41 | app.patch('/api/magazines/:id', (req, res) => { 42 | const { body } = req 43 | const magazine = magazines.find(a => a.id.toString() === req.params.id) 44 | const index = magazines.indexOf(magazine) 45 | 46 | if (index >= 0) { 47 | magazine.name = body.name 48 | magazine.editorial = body.editorial 49 | magazine.issue = body.issue 50 | magazine.publisher = body.publisher 51 | magazines[index] = magazine 52 | } 53 | 54 | res.json(magazine) 55 | }) 56 | 57 | app.put('/api/magazines/:id', (req, res) => { 58 | const { body } = req 59 | const magazine = magazines.find(a => a.id.toString() === req.params.id) 60 | const index = magazines.indexOf(magazine) 61 | 62 | if (index >= 0) { 63 | magazine.name = body.name 64 | magazine.editorial = body.editorial 65 | magazine.issue = body.issue 66 | magazine.publisher = body.publisher 67 | magazines[index] = magazine 68 | } 69 | 70 | res.json(magazine) 71 | }) 72 | 73 | app.delete('/api/magazines/:id', (req, res) => { 74 | const magazine = magazines.find(a => a.id.toString() === req.params.id) 75 | const index = magazines.indexOf(magazine) 76 | 77 | if (index >= 0) magazines.splice(index, 1) 78 | 79 | res.status(202).send() 80 | }) 81 | 82 | app.post('/api/magazines', (req, res) => { 83 | const id = magazines[magazines.length - 1].id + 1 84 | const { body } = req 85 | 86 | const magazine = { 87 | id, 88 | name: body.name, 89 | editorial: body.editorial, 90 | issue: body.issue, 91 | publisher: body.publisher, 92 | } 93 | 94 | magazines.push(magazine) 95 | 96 | res.status(201).send(magazine) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | transpileDependencies: ['vuetify'], 5 | css: { 6 | loaderOptions: { 7 | sass: { 8 | implementation: require('sass'), 9 | fiber: require('fibers'), 10 | }, 11 | }, 12 | }, 13 | configureWebpack: { 14 | resolve: { 15 | alias: { 16 | '@assets': path.resolve(__dirname, 'src/assets'), 17 | '@components': path.resolve(__dirname, 'src/components'), 18 | '@constants': path.resolve(__dirname, 'src/constants'), 19 | '@demo': path.resolve(__dirname, 'demo'), 20 | '@e2e': path.resolve(__dirname, 'tests/e2e'), 21 | '@handlers': path.resolve(__dirname, 'src/handlers'), 22 | '@plugins': path.resolve(__dirname, 'src/plugins'), 23 | '@router': path.resolve(__dirname, 'src/router'), 24 | '@store': path.resolve(__dirname, 'src/store'), 25 | '@templates': path.resolve(__dirname, 'src/templates/src'), 26 | '@va-auth': path.resolve(__dirname, 'src/va-auth/src'), 27 | '@validators': path.resolve(__dirname, 'src/validators/src'), 28 | }, 29 | }, 30 | }, 31 | } 32 | --------------------------------------------------------------------------------