├── .browserslistrc ├── .env ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── css │ └── task.css ├── favicon.ico ├── img │ └── bg.jpg └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Header.vue │ ├── Modals │ │ ├── CreateSubtask.vue │ │ ├── CreateTask.vue │ │ ├── UpdateSubtask.vue │ │ └── UpdateTask.vue │ └── TaskList.vue ├── main.js ├── mixins │ └── guid.js └── store │ ├── index.js │ └── modules │ ├── all.js │ ├── messages.js │ ├── subtask.js │ └── task.js └── tests └── unit ├── components └── header.spec.js └── taskList.spec.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_NAME=Vue Task Manager by Mustafa Çağrı Güven -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/__tests__/*.{j,t}s?(x)', 21 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 22 | ], 23 | env: { 24 | jest: true 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | .github 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Firebase 26 | .firebaserc 27 | firebase.json 28 | *.firebase -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Task Manager 2 | ![altcenter](https://www.mustafacagri.com/wp-content/uploads/2020/11/vue-task-manager-create-task.gif "Vue Task Manager - Create a Task") 3 | 4 | ![altcenter](https://www.mustafacagri.com/wp-content/uploads/2020/11/vue-task-manager-create-subtask.gif "Vue Task Manager - Create a Subtask") 5 | 6 | ![altcenter](https://www.mustafacagri.com/wp-content/uploads/2020/11/vue-task-manager-pagination-delete.gif "Vue Task Manager - Pagination and Delete Task") 7 | 8 | ## Demo 9 | https://vue-js-task-manager.web.app/ 10 | 11 | ## Used 12 | Vuex with modules, Localstorage (It is ready to use Firebase database), Mixins, Lodash, Test (Jest) 13 | 14 | ## Project setup 15 | ``` 16 | npm install 17 | ``` 18 | 19 | ### Compiles and hot-reloads for development 20 | ``` 21 | npm run serve 22 | ``` 23 | 24 | ### Compiles and minifies for production 25 | ``` 26 | npm run build 27 | ``` 28 | 29 | # Challenge 30 | 31 | The coding challenge is to create a single-page app using Vue.js and Vuex. You are free to use vue-cli while creating application. 32 | 33 | In this assignment, we would like you to create a simple task manager web application. 34 | You will perform some operations on tasks and subtasks. You can find the details on below. 35 | We expect a cross-browser compatible and responsive web application that matching the requirements below. 36 | 37 | We will appreciate that if you add unit-tests and comments for your project (if applicable). 38 | We expect you to write a README.md document. 39 | 40 | # Challenge Scenario - Task Board 41 | 42 | With this assignment, we want to create a board for our tasks. Tasks have name, state and levels properties that could be managed by users. Tasks could have below 43 | levels with the given priority order. 44 | - Blocker 45 | - Critical 46 | - High 47 | - Low 48 | 49 | Tasks could have subtasks. Subtask have same properties of tasks and also parent task. 50 | We have two states for tasks that are "Open" and "Closed". 51 | 52 | 53 | ## Rules 54 | You could choose intuitive way for not described points specifically. 55 | ### Creation Rules 56 | - Task could be created with required fields name and level. 57 | - Task name need to be unique and max 25 character. 58 | - Default state is open. 59 | - Level should be selected, no default value. 60 | - Subtask creation enabled if user select/focus on a task. 61 | - Task could have 4 sub-task at most. 62 | - Task could have no subtask. 63 | - The application will not have any backend so for storing tasks and subtasks you should use local storage in case of refreshing application pages in the browser. 64 | 65 | ### List Rules 66 | - There are two lists that open and closed tasks with priority order. 67 | - Tasks need to be sorted high to low priority. 68 | - If two tasks have same priority, first created one has high priority. 69 | - Subtasks need to be sorted high to low priority under task. 70 | - Lists need to be automatically updated if any task or subtask property is changed. 71 | - Task and subtask cards have color depending on their priority. 72 | - List cards layout need to be responsive to width of page. For example, If page could show 3 cards in a row, page need to show 2 open card column and 1 closed 73 | task columns. Open task list has higher priority to column enlarge. 74 | - User could paginate lists. 75 | 76 | ## Update Rules 77 | - Properties modal open with a task or subtask for updating. 78 | - Task name and parent tasks are showed as read-only not updatable. 79 | - Levels and state are updatable. 80 | - Tasks could not be closed if any subtask is still open. 81 | - If any subtask of task set open, task is set as open too. 82 | - If any task is deleted, all subtasks are also deleted. 83 | 84 | 85 | 86 | 87 | # Technology Stack 88 | - VueJs version2 89 | - Vuex 90 | - CSS3/SASS 91 | - ES6+ 92 | - Mixins (If applicable) 93 | - Use the related technologies for build configuration to create production ready bundle. 94 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest' 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-task-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "s": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "tuw": "vue-cli-service test:unit --watch", 11 | "ttl": "vue-cli-service test:unit taskList --watch", 12 | "lint": "vue-cli-service lint" 13 | }, 14 | "dependencies": { 15 | "bootstrap": "^4.5.3", 16 | "bootstrap-vue": "^2.19.0", 17 | "core-js": "^3.6.5", 18 | "lodash": "^4.17.20", 19 | "vue": "^2.6.12", 20 | "vue-select": "^3.10.8", 21 | "vuelidate": "^0.7.6", 22 | "vuex": "^3.4.0" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.5.0", 26 | "@vue/cli-plugin-eslint": "~4.5.0", 27 | "@vue/cli-plugin-unit-jest": "~4.5.0", 28 | "@vue/cli-plugin-vuex": "~4.5.0", 29 | "@vue/cli-service": "~4.5.0", 30 | "@vue/test-utils": "^1.0.3", 31 | "babel-eslint": "^10.1.0", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-vue": "^6.2.2", 34 | "vue-template-compiler": "^2.6.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/css/task.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | --red:#ED1C24;--darkred:#dc3545;--darkblue:#212529; 3 | --blocker: var(--purple); 4 | --critical: var(--danger); 5 | --high: #e67e22; 6 | --low: #2ecc71; 7 | } 8 | .container-fluid{max-width:1600px} 9 | body{background:#34495e;height:100%;background:url(../img/bg.jpg);background-size:auto;margin:20px;font-family:'Open Sans',sans-serif;} 10 | body::-webkit-scrollbar-track 11 | { 12 | /*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);*/ 13 | border-radius: 0px; 14 | background-color: #F5F5F5; 15 | } 16 | 17 | body::-webkit-scrollbar 18 | { 19 | width: 12px; 20 | background-color: #F5F5F5; 21 | } 22 | 23 | body::-webkit-scrollbar-thumb 24 | { 25 | border-radius: 0px; 26 | /*-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);*/ 27 | background-color: var(--mainColor2); 28 | } 29 | 30 | img{max-width:100%} 31 | .mainbox{position:relative;display:block;width:100%;background-color:#fff;color:var(--darkBlue);word-wrap: break-word;padding:0;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;} 32 | .mainbox>.title{display: flex; align-items: center; position:relative;font-weight:700;font-size:1rem;color:#212529;padding:10px 15px;line-height:2rem;border-bottom:1px solid #efefef;font-family:'Roboto',sans-serif} 33 | .mainbox>.title.withoutline{border-bottom:0} 34 | .mainbox>.title>.right{margin-left:auto; border-left:1px solid #e8ebf5;padding-left:15px;color:var(--darkBlue)} 35 | .mainbox>.title>.right:last-child{border-left:0px;padding-left:0;} 36 | .mainbox.bottomredborder {border-bottom: 2px solid var(--red);} 37 | 38 | .mainbox>.title>.right:hover{color:var(--red);text-decoration:none} 39 | .mainbox>.title>.right:after{content:"";clear:both;display:Block} 40 | .mainbox>.title>svg{color:var(--red);width:30px} 41 | .mainbox>.title:not(.withoutline):before{bottom:-1px;width:120px;height:1px;background:var(--darkred);content:'';left:0;position:absolute} 42 | .mainbox>.title:after{content:"";clear:both;display:Block} 43 | .mainbox>.content{z-index:9;position:relative;padding:15px 20px} 44 | 45 | .clickable {cursor: pointer;} 46 | 47 | .modal-backdrop {opacity: 0.9 !important;} 48 | 49 | .btn-Blocker {border:1px solid var(--blocker)!important; color:var(--blocker)!important;} 50 | .btn-Critical {border:1px solid var(--critical)!important; color:var(--critical)!important;} 51 | .btn-High {border:1px solid var(--high)!important; color:var(--high)!important;} 52 | .btn-Low {border:1px solid var(--low)!important; color:var(--low)!important;} 53 | 54 | .btn-Open {border:1px solid var(--success)!important; color:var(--success)!important;} 55 | .btn-Closed {border:1px solid var(--danger)!important; color:var(--danger)!important;} 56 | 57 | .btn-Blocker:hover, .btn-Blocker.selected {background-color:var(--blocker)!important; color:#fff!important;} 58 | .btn-Critical:hover, .btn-Critical.selected {background-color:var(--critical)!important; color:#fff!important;} 59 | .btn-High:hover, .btn-High.selected {background-color:var(--high)!important; color:#fff!important;} 60 | .btn-Low:hover, .btn-Low.selected {background-color:var(--low)!important; color:#fff!important;} 61 | 62 | .btn-Open:hover, .btn-Open.selected {background-color:var(--success)!important; color:#fff!important;} 63 | .btn-Closed:hover, .btn-Closed.selected {background-color:var(--danger)!important; color:#fff!important;} 64 | 65 | 66 | /* taskList */ 67 | .taskList .badge.Blocker {background-color:var(--blocker);} 68 | .taskList .badge.Critical {background-color:var(--critical);} 69 | .taskList .badge.High {background-color:var(--high);} 70 | .taskList .badge.Low {background-color:var(--low);} 71 | 72 | .taskList .title > span > i {margin-right:310px;} 73 | 74 | .taskList .title > span.Blocker {color:var(--blocker);} 75 | .taskList .title > span.Critical {color:var(--critical);} 76 | .taskList .title > span.High {color:var(--high);} 77 | .taskList .title > span.Low {color:var(--low);} 78 | 79 | .taskList .mainbox>.title:not(.withoutline):before{background:var(--dark)!important;} 80 | .taskList .mainbox.Blocker>.title:not(.withoutline):before{background:var(--blocker)!important;} 81 | .taskList .mainbox.Critical>.title:not(.withoutline):before{background:var(--critical)!important;} 82 | .taskList .mainbox.High>.title:not(.withoutline):before{background:var(--high)!important;} 83 | .taskList .mainbox.Low>.title:not(.withoutline):before{background:var(--low)!important;} 84 | 85 | .taskList .mainbox {margin-bottom:20px;} 86 | .taskList .mainbox > .content {font-size:0.9rem;} 87 | 88 | .taskList .subTasks {display: flex; flex-wrap: wrap; width: 100%; margin-top:15px; border-top:1px solid #efefef; padding-top:15px;} 89 | 90 | 91 | .taskList .subTasks > .subTask {width: 48%; background-color:#f5f6fa; border-left:2px solid #ddd; margin-bottom:10px; padding:10px 15px; border-radius: 5px; font-size:.9rem;} 92 | .taskList .subTasks > .subTask:nth-child(2n+1) {margin-right:4%;} 93 | 94 | 95 | 96 | .taskList .subTasks .subTask > .title {margin-bottom:10px; font-weight:600;} 97 | .taskList .subTasks .subTask > .content {font-size:.8rem; color:#333; display:block; clear:both; word-wrap: break-word;} 98 | 99 | 100 | .taskList .subTasks .subTask.Blocker {border-color:var(--blocker);} 101 | .taskList .subTasks .subTask.Critical {border-color:var(--critical);} 102 | .taskList .subTasks .subTask.High {border-color:var(--high);} 103 | .taskList .subTasks .subTask.Low {border-color:var(--low);} 104 | 105 | .taskList .createSubTask:hover {cursor: pointer;} 106 | 107 | .taskList .pagination .btn:not(:last-child) {margin-right:10px;} 108 | 109 | .taskList .closedSubtask {opacity: 0.3;} 110 | 111 | @media screen and (max-width: 1199px) { 112 | .taskList:not(.important) .subTasks > .subTask {width: 100%;} 113 | .taskList:not(.important) .subTasks > .subTask:nth-child(2n+1) {margin-right:0%;} 114 | } 115 | @media screen and (max-width: 1024px) { 116 | .taskList.important .subTasks > .subTask {width: 100%;} 117 | .taskList.important .subTasks > .subTask:nth-child(2n+1) {margin-right:0%;} 118 | } 119 | 120 | /* taskList */ 121 | 122 | 123 | @media only screen and (max-width: 692px) { 124 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue-task-manager/cfc7d14a09dd1c569742abf37e395a916373d225/public/favicon.ico -------------------------------------------------------------------------------- /public/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue-task-manager/cfc7d14a09dd1c569742abf37e395a916373d225/public/img/bg.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= process.env.VUE_APP_NAME %> 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 50 | 51 | 71 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seniorvuejsdeveloper/vue-task-manager/cfc7d14a09dd1c569742abf37e395a916373d225/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 14 | 23 | -------------------------------------------------------------------------------- /src/components/Modals/CreateSubtask.vue: -------------------------------------------------------------------------------- 1 | 48 | 107 | -------------------------------------------------------------------------------- /src/components/Modals/CreateTask.vue: -------------------------------------------------------------------------------- 1 | 26 | 70 | -------------------------------------------------------------------------------- /src/components/Modals/UpdateSubtask.vue: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /src/components/Modals/UpdateTask.vue: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /src/components/TaskList.vue: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | Vue.config.productionTip = false 5 | 6 | import { BootstrapVue } from 'bootstrap-vue' 7 | import 'bootstrap/dist/css/bootstrap.css' 8 | import 'bootstrap-vue/dist/bootstrap-vue.css' 9 | import Vuelidate from 'vuelidate' 10 | import vSelect from 'vue-select' 11 | 12 | Vue.use(BootstrapVue) 13 | Vue.use(Vuelidate) 14 | Vue.component('v-select', vSelect) 15 | import 'vue-select/dist/vue-select.css'; 16 | 17 | new Vue({ 18 | store, 19 | render: h => h(App) 20 | }).$mount('#app') 21 | -------------------------------------------------------------------------------- /src/mixins/guid.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'guid', 3 | methods: { 4 | guidGenerator(){ 5 | let lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); } 6 | let d0 = Math.random()*0xffffffff|0; 7 | let d1 = Math.random()*0xffffffff|0; 8 | let d2 = Math.random()*0xffffffff|0; 9 | let d3 = Math.random()*0xffffffff|0; 10 | return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+ 11 | lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+ 12 | lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+ 13 | lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff]; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import messages from "./modules/messages"; 4 | import task from "./modules/task"; 5 | import subtask from "./modules/subtask"; 6 | import all from "./modules/all"; 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | namespaced: true, 12 | modules:{ 13 | messages, task, subtask, all 14 | }, 15 | 16 | state: { 17 | }, 18 | 19 | mutations: { 20 | }, 21 | 22 | actions: { 23 | }, 24 | 25 | getters: { 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/modules/all.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | const state = { 3 | definedValues: { 4 | maxTaskDescription: 100, 5 | maxSubtaskLength: 4, 6 | pagingSizes: [2,5,10,25,50] 7 | }, 8 | states: [{id:0, name:"Closed"}, {id:1, name:"Open"},], 9 | levels: [ 10 | {id:0, name:"Low"}, 11 | {id:1, name:"High"}, 12 | {id:2, name:"Critical"}, 13 | {id:3, name:"Blocker"}, 14 | ] 15 | } 16 | 17 | const getters = { 18 | getStateNameById: (state) => (payload) => { 19 | let foundState = state.states.find(x => x.id == payload) 20 | //this.state.all.levels 21 | if(foundState){ 22 | return foundState.name 23 | } 24 | return null 25 | }, 26 | getLevels(state){ 27 | return state.levels 28 | }, 29 | getLevel(state,payload){ 30 | return state.levels.find(x => x.id === payload) 31 | }, 32 | 33 | 34 | getDefinedValues(state){ 35 | return state.definedValues 36 | }, 37 | getMaxSubtaskLength(state){ 38 | return state.definedValues.maxSubtaskLength 39 | }, 40 | getStates(state){ 41 | return state.states 42 | } 43 | } 44 | 45 | const mutations = { 46 | init(){ 47 | this.state.task.taskList = JSON.parse(localStorage.getItem('taskList')) 48 | }, 49 | updateStorage(){ 50 | this.state.task.taskList = _.orderBy(this.state.task.taskList, ['levelId', 'time'], ['desc', 'asc']) 51 | localStorage.setItem("taskList", JSON.stringify(this.state.task.taskList)) 52 | }, 53 | updateSubTaskStorage(state,payload){ 54 | let index = this.state.task.taskList.findIndex(x => x.id == payload) 55 | this.state.task.taskList[index].subTasks = _.orderBy(this.state.task.taskList[index].subTasks, ['levelId', 'time'], ['desc', 'asc']) 56 | } 57 | } 58 | 59 | const actions = { 60 | } 61 | 62 | export default { 63 | namespaced: true, 64 | state, 65 | getters, 66 | mutations, 67 | actions 68 | } 69 | -------------------------------------------------------------------------------- /src/store/modules/messages.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | error: null, 3 | isSucess: null, 4 | errorTime: 5000, 5 | successTime: 5000 6 | } 7 | 8 | const getters = { 9 | getError(state) { 10 | return state.error 11 | }, 12 | getIsSuccess(state){ 13 | return state.isSucess 14 | }, 15 | } 16 | 17 | const mutations = { 18 | clearError(state){ 19 | state.error = null 20 | }, 21 | setError(state, payload){ 22 | state.error = payload 23 | }, 24 | setIsSuccess(state, payload){ 25 | state.isSucess = payload 26 | }, 27 | clearIsSuccess(state){ 28 | state.isSucess = null 29 | }, 30 | } 31 | 32 | const actions = { 33 | setErrorClear(context,payload){ 34 | context.commit("setError", payload.error) 35 | let time = (payload.time && payload.time!=undefined) ? payload.time : state.errorTime 36 | setTimeout(() => {context.commit("clearError")}, time) 37 | }, 38 | setError(context,payload){ 39 | try { 40 | context.commit("clearIsSuccess") 41 | context.dispatch("setErrorClear", payload) 42 | } catch (error) { 43 | context.dispatch("setErrorClear", {error}) 44 | } 45 | }, 46 | 47 | 48 | setIsSuccessClear(context,payload){ 49 | context.commit("setIsSuccess", payload.message) 50 | let time = (payload.time && payload.time!=undefined) ? payload.time : state.successTime 51 | setTimeout(() => {context.commit("clearIsSuccess")}, time) 52 | console.log(payload) 53 | }, 54 | setIsSuccess(context,payload){ 55 | console.log(payload) 56 | try { 57 | context.commit("clearError") 58 | context.dispatch("setIsSuccessClear", payload) 59 | } catch (error) { 60 | context.dispatch("setErrorClear", {error}) 61 | } 62 | } 63 | } 64 | 65 | export default { 66 | namespaced: true, 67 | state, 68 | getters, 69 | mutations, 70 | actions 71 | } 72 | -------------------------------------------------------------------------------- /src/store/modules/subtask.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | const state = { 3 | selectedSubtask: null, 4 | } 5 | 6 | const getters = { 7 | getSelectedSubtask(state){ 8 | return state.selectedSubtask 9 | }, 10 | } 11 | 12 | const mutations = { 13 | addSubtask(state,payload){ 14 | let level = this.state.all.levels.find(x => x.id == payload.levelId) 15 | if(level){ 16 | payload.level = level.name 17 | payload.time = _.now() 18 | this.state.task.taskList.find(x => x.id == payload.parentId).subTasks.push(payload) 19 | } 20 | }, 21 | setSelectedSubtask(state,payload){ 22 | state.selectedSubtask = payload 23 | }, 24 | updateSubtask(state,payload){ 25 | console.log(payload) 26 | console.log(this.state.task.taskList[payload.parentIndex]) 27 | 28 | this.state.task.taskList[payload.parentIndex].subTasks[payload.index].level = this.state.all.levels.find(x => x.id == payload.levelId).name 29 | this.state.task.taskList[payload.parentIndex].subTasks[payload.index].state = payload.state 30 | this.state.task.taskList[payload.parentIndex].subTasks[payload.index].levelId = payload.levelId 31 | this.state.task.taskList[payload.parentIndex].subTasks[payload.index].description = payload.description 32 | }, 33 | deleteSubtask(state, payload){ 34 | this.state.task.taskList[payload.parentIndex].subTasks.splice(payload.index, 1) 35 | } 36 | } 37 | 38 | const actions = { 39 | async addSubtask({dispatch,commit},payload){ 40 | let parent = this.state.task.taskList.find(x => x.id == payload.parentId) 41 | if(parent){ 42 | // console.log(this.getters.task.getSelectedTaskId) 43 | console.log(this.getters["task/getTaskById"](parent.id)) 44 | console.log(this.getters["task/getSelectedTaskId"]) 45 | 46 | if(this.getters["task/getTaskById"](parent.id).subTasks == undefined || this.getters["task/getTaskById"](parent.id).subTasks.length x.id == payload.parentId) 60 | if(parentIndex == -1) { 61 | context.dispatch("messages/setError", {error:"Parent task could not find! Something is totally wrong!"}, {root: true}) 62 | } 63 | else{ 64 | payload.parentIndex = parentIndex 65 | } 66 | let index = this.state.task.taskList[parentIndex].subTasks.findIndex(x => x.id == payload.id) 67 | if(index == -1) { 68 | context.dispatch("messages/setError", {error:"Subtask could not find! Something is totally wrong!"}, {root: true}) 69 | } 70 | else { 71 | payload.index = index 72 | if(payload.state==0){// open/close checking for parent task 73 | let isClosed = true 74 | this.state.task.taskList[parentIndex].subTasks.forEach(item => { 75 | if(item.state==1){ 76 | isClosed = false 77 | } 78 | }) 79 | if(isClosed){ 80 | this.state.task.taskList[parentIndex].state=0 81 | } 82 | } 83 | else{ 84 | this.state.task.taskList[parentIndex].state = 1 85 | } 86 | await context.commit("updateSubtask", payload) 87 | context.commit("messages/clearError", null, {root: true}) 88 | context.dispatch("messages/setIsSuccess", {message:"Your subtask is successfully updated!"}, {root: true}) 89 | context.commit("all/updateStorage", null, {root: true}) 90 | } 91 | } catch (error) { 92 | context.dispatch("messages/setError", {error:error}, {root: true}) 93 | } 94 | }, 95 | async deleteSubTask(context, payload){ 96 | try { 97 | let parentIndex = this.state.task.taskList.findIndex(x => x.id == payload.parentId) 98 | if(parentIndex == -1) { 99 | context.dispatch("messages/setError", {error:"Parent task could not find! Something is totally wrong!"}, {root: true}) 100 | } 101 | 102 | let index = this.state.task.taskList[parentIndex].subTasks.findIndex(x => x.id == payload.id) 103 | if(index == -1) { 104 | context.dispatch("messages/setError", {error:"Subtask could not find! Something is totally wrong!"}, {root: true}) 105 | } 106 | else { 107 | payload = {parentIndex, index} 108 | await context.commit("deleteSubtask", payload) 109 | context.commit("messages/clearError", null, {root: true}) 110 | context.dispatch("messages/setIsSuccess", {message:"Your subtask is successfully deleted!"}, {root: true}) 111 | context.commit("all/updateStorage", null, {root: true}) 112 | } 113 | } catch (error) { 114 | context.dispatch("messages/setError", {error:error}, {root: true}) 115 | } 116 | } 117 | } 118 | 119 | export default { 120 | namespaced: true, 121 | state, 122 | getters, 123 | mutations, 124 | actions 125 | } 126 | -------------------------------------------------------------------------------- /src/store/modules/task.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | const state = { 3 | selectedTaskId: null, 4 | selectedTask: null, 5 | taskList: null 6 | } 7 | 8 | const getters = { 9 | getTasks(state){ 10 | return state.taskList 11 | }, 12 | getTaskListByState: (state) => (payload) => { 13 | if(state.taskList){ 14 | return state.taskList.filter(x => x.state === payload) 15 | } 16 | else{ 17 | return null 18 | } 19 | }, 20 | getTaskById: (state) => (payload) => { 21 | return state.taskList.find(x => x.id == payload) 22 | }, 23 | getSelectedTaskId(state){ 24 | return state.selectedTaskId 25 | }, 26 | getSelectedTask(state){ 27 | return state.selectedTask 28 | }, 29 | getTaskNameAndId(state){ 30 | return _.map(state.taskList, _.partialRight(_.pick, ['id', 'name'])) 31 | } 32 | } 33 | 34 | const mutations = { 35 | setSelectedTaskId(state,payload){ 36 | state.selectedTaskId = payload 37 | }, 38 | addTask(state,payload){ 39 | let level = this.state.all.levels.find(x => x.id == payload.levelId) 40 | if(level){ 41 | payload.level = level.name 42 | payload.time = _.now() 43 | if(!state.taskList){ 44 | state.taskList = new Array() 45 | } 46 | state.taskList.push(payload) 47 | } 48 | }, 49 | updateTask(state,payload){ 50 | state.taskList[payload.index].level = this.state.all.levels.find(x => x.id == payload.levelId).name 51 | state.taskList[payload.index].state = payload.state 52 | state.taskList[payload.index].levelId = payload.levelId 53 | state.taskList[payload.index].description = payload.description 54 | }, 55 | setSelectedTask(state,payload){ 56 | state.selectedTask = payload 57 | }, 58 | deleteTask(state, payload){ 59 | this.state.task.taskList.splice(payload.index, 1) 60 | } 61 | } 62 | 63 | const actions = { 64 | async addTask(context,payload){ 65 | let okay = false, found 66 | if(!context.state.taskList){ 67 | okay = true 68 | } 69 | else{ 70 | if(context.state.taskList){ 71 | found = context.state.taskList.find(x => x.name == payload.name) 72 | if(!found){okay = true} 73 | } 74 | } 75 | if(okay){ 76 | await context.commit("addTask",payload) 77 | context.commit("all/updateStorage", null, {root: true}) 78 | } 79 | else{ 80 | context.dispatch("messages/setError", {error:`You already have a task named "${payload.name}".`}, {root: true}) 81 | } 82 | }, 83 | async updateTask(context, payload){ 84 | try { 85 | let index = context.state.taskList.findIndex(x => x.id == payload.id) 86 | if(index == -1) { 87 | context.dispatch("messages/setError", {error:"Task could not find! Something is totally wrong!"}, {root: true}) 88 | } 89 | else { 90 | payload.index = index 91 | await context.commit("updateTask", payload) 92 | context.commit("messages/clearError", null, {root: true}) 93 | context.dispatch("messages/setIsSuccess", {message:"Your task is successfully updated!"}, {root: true}) 94 | context.commit("all/updateStorage", null, {root: true}) 95 | } 96 | } catch (error) { 97 | context.dispatch("messages/setError", {error}, {root: true}) 98 | } 99 | }, 100 | async deleteTask(context, payload){ 101 | console.log("deleteTask") 102 | try { 103 | let index = this.state.task.taskList.findIndex(x => x.id == payload.id) 104 | if(index == -1) { 105 | context.dispatch("messages/setError", {error:"Task could not find! Something is totally wrong!"}, {root: true}) 106 | } 107 | else { 108 | payload = {index} 109 | await context.commit("deleteTask", payload) 110 | context.commit("messages/clearError", null, {root: true}) 111 | context.dispatch("messages/setIsSuccess", {message:"Your subtask is successfully deleted!"}, {root: true}) 112 | context.commit("all/updateStorage", null, {root: true}) 113 | } 114 | } catch (error) { 115 | context.dispatch("messages/setError", {error:error}, {root: true}) 116 | } 117 | } 118 | } 119 | 120 | export default { 121 | namespaced: true, 122 | state, 123 | getters, 124 | mutations, 125 | actions 126 | } 127 | -------------------------------------------------------------------------------- /tests/unit/components/header.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex" 2 | import { mount, createLocalVue } from "@vue/test-utils" 3 | import App from "@/components/header.vue" 4 | 5 | const localVue = createLocalVue() 6 | localVue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | task:{ 11 | namespaced: true, 12 | getters:{ 13 | getTasks(state){ 14 | return state.taskList 15 | } 16 | } 17 | } 18 | } 19 | }) 20 | 21 | describe("App", () => { 22 | it("renders the header using a Vuex store", () => { 23 | const wrapper = mount(App, { 24 | store, 25 | localVue 26 | }) 27 | expect(wrapper.find(".btn-success").text()).toBe("Create a Task") 28 | }) 29 | }) -------------------------------------------------------------------------------- /tests/unit/taskList.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex" 2 | import { mount, createLocalVue, shallowMount } from "@vue/test-utils" 3 | import TaskList from "@/components/TaskList.vue" 4 | 5 | const localVue = createLocalVue() 6 | localVue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | modules: { 10 | all:{ 11 | namespaced: true, 12 | state: { 13 | definedValues: { 14 | maxTaskDescription: 100, 15 | maxSubtaskLength: 4, 16 | pagingSizes: [2,5,10,25,50] 17 | }, 18 | states: [{id:0, name:"Closed"}, {id:1, name:"Open"},], 19 | levels: [ 20 | {id:0, name:"Low"}, 21 | {id:1, name:"High"}, 22 | {id:2, name:"Critical"}, 23 | {id:3, name:"Blocker"}, 24 | ], 25 | taskList: [{"id":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","name":"Parent 1","levelId":3,"description":"Parent 1 Description","state":1,"subTasks":[{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"c71c9c8f-c12e-4a4a-b963-6179b6a6cc68","name":"Child 1","levelId":3,"description":"Child 1 Description","state":1,"level":"Blocker","time":1606156864333,"parentName":"Parent 1"},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"19329621-1698-48ba-a892-1b8f2410d727","name":"Child 4","levelId":3,"description":"If two tasks have same priority, first created one has high priority.","state":1,"level":"Blocker","time":1606157284498},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"4a182434-2515-4fcf-a3e0-bae9b56744fd","name":"Child 2","levelId":2,"description":" Description","state":1,"level":"Critical","time":1606156872450},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"aa649a41-c1e1-4a5d-8195-54bde00e0d90","name":"Child 3","levelId":1,"description":"Child 3 description and description aaaannnnnndddd...","state":1,"level":"High","time":1606157230954,"parentName":"Parent 1"}],"level":"Blocker","time":1606156625875},{"id":"bff03e61-9562-4478-aadc-5e6c1222921d","name":"Parent 4 is Closed?","levelId":2,"description":"Is this task closed?","state":0,"subTasks":[],"level":"Critical","time":1606157443720},{"id":"2a508064-85ee-4b3a-9121-a9b7bf156152","name":"Parent 5 Has Only One...","levelId":2,"description":null,"state":1,"subTasks":[{"parentId":"2a508064-85ee-4b3a-9121-a9b7bf156152","id":"55afcb4e-429f-44bd-ba63-3a4e8e29d634","name":"Child 5","levelId":0,"description":"It is not so important...","state":1,"level":"Low","time":1606157536603}],"level":"Critical","time":1606157501896},{"id":"d699bd43-5d72-404b-b51a-2452948c1c53","name":"Parent 3 High!","levelId":1,"description":"It's a high level task! Be awere of this!","state":1,"subTasks":[],"level":"High","time":1606157360001},{"id":"77c3ff59-fd18-46f4-8d3a-e0ab769c4d64","name":"Parent 2","levelId":0,"description":"Parent 2 is a low level task...","state":1,"subTasks":[],"level":"Low","time":1606156635770}] 26 | }, 27 | getters:{ 28 | getStateNameById: (state) => (payload) => { 29 | let foundState = state.states.find(x => x.id == payload) 30 | //this.state.all.levels 31 | if(foundState){ 32 | return foundState.name 33 | } 34 | return null 35 | }, 36 | getDefinedValues(state){ 37 | return state.definedValues 38 | }, 39 | getMaxSubtaskLength(state){ 40 | return state.definedValues.maxSubtaskLength 41 | } 42 | } 43 | } 44 | } 45 | }) 46 | describe("TaskList", () => { 47 | it("renders the TaskList using a real Vuex store", () => { 48 | const wrapper = shallowMount(TaskList, { 49 | store, 50 | localVue , 51 | propsData: { 52 | state:"1", 53 | taskList: [{"id":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","name":"Parent 1","levelId":3,"description":"Parent 1 Description","state":1,"subTasks":[{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"c71c9c8f-c12e-4a4a-b963-6179b6a6cc68","name":"Child 1","levelId":3,"description":"Child 1 Description","state":1,"level":"Blocker","time":1606156864333,"parentName":"Parent 1"},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"19329621-1698-48ba-a892-1b8f2410d727","name":"Child 4","levelId":3,"description":"If two tasks have same priority, first created one has high priority.","state":1,"level":"Blocker","time":1606157284498},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"4a182434-2515-4fcf-a3e0-bae9b56744fd","name":"Child 2","levelId":2,"description":" Description","state":1,"level":"Critical","time":1606156872450},{"parentId":"e18f6c0a-f3f6-4139-8752-3d30b6ad0cd2","id":"aa649a41-c1e1-4a5d-8195-54bde00e0d90","name":"Child 3","levelId":1,"description":"Child 3 description and description aaaannnnnndddd...","state":1,"level":"High","time":1606157230954,"parentName":"Parent 1"}],"level":"Blocker","time":1606156625875},{"id":"bff03e61-9562-4478-aadc-5e6c1222921d","name":"Parent 4 is Closed?","levelId":2,"description":"Is this task closed?","state":0,"subTasks":[],"level":"Critical","time":1606157443720},{"id":"2a508064-85ee-4b3a-9121-a9b7bf156152","name":"Parent 5 Has Only One...","levelId":2,"description":null,"state":1,"subTasks":[{"parentId":"2a508064-85ee-4b3a-9121-a9b7bf156152","id":"55afcb4e-429f-44bd-ba63-3a4e8e29d634","name":"Child 5","levelId":0,"description":"It is not so important...","state":1,"level":"Low","time":1606157536603}],"level":"Critical","time":1606157501896},{"id":"d699bd43-5d72-404b-b51a-2452948c1c53","name":"Parent 3 High!","levelId":1,"description":"It's a high level task! Be awere of this!","state":1,"subTasks":[],"level":"High","time":1606157360001},{"id":"77c3ff59-fd18-46f4-8d3a-e0ab769c4d64","name":"Parent 2","levelId":0,"description":"Parent 2 is a low level task...","state":1,"subTasks":[],"level":"Low","time":1606156635770}] 54 | }, 55 | data: function() { 56 | return { 57 | stateName: 'Open', 58 | paginationCurrent: 1, 59 | paginationSize: 1 60 | } 61 | } 62 | }) 63 | expect(wrapper.html()).toContain(" Parent 4 is Closed?") 64 | // expect(wrapper.find(".btn.btn-sm").classes()).toBe("1") 65 | }) 66 | 67 | }) --------------------------------------------------------------------------------