├── .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 | 
3 |
4 | 
5 |
6 | 
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 |
2 |
3 |
4 |
{{getError}}
5 |
{{getIsSuccess}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
It's time to start now!
14 |
15 | There is no tasks right now! You may start with to create a new one!
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
2 |
13 |
14 |
23 |
--------------------------------------------------------------------------------
/src/components/Modals/CreateSubtask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
{{description.length}} / {{$v.description.$params.maxLength.max}}
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
107 |
--------------------------------------------------------------------------------
/src/components/Modals/CreateTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{description.length}} / {{$v.description.$params.maxLength.max}}
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
70 |
--------------------------------------------------------------------------------
/src/components/Modals/UpdateSubtask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{getError}}
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
{{description.length}} / {{$v.description.$params.maxLength.max}}
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/Modals/UpdateTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{getError}}
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{{description.length}} / {{$v.description.$params.maxLength.max}}
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/TaskList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{stateName}} Tasks
6 |
7 |
10 |
11 |
12 |
There is no task tagged with {{stateName}}
13 |
14 |
15 |
16 |
17 |
18 |
{{task.name}}
19 |
20 |
21 |
22 |
23 | {{task.level}}
24 |
25 |
26 |
27 | {{task.description}}
28 |
29 |
30 |
31 | {{subTask.name}}
32 |
33 |
34 |
35 |
36 |
{{subTask.description}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
50 |
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 | })
--------------------------------------------------------------------------------