From 2f025127cc200e26afbe51e6306758110555c3c9 Mon Sep 17 00:00:00 2001 From: oliveryh Date: Thu, 6 Aug 2020 19:49:51 +0100 Subject: [PATCH 01/13] :sparkles: Basic task timer (close #6) --- client/src/components/Task.vue | 133 +++++++++++++++++++++++++++ client/src/components/TaskList.vue | 57 ++---------- client/src/store/actions.type | 2 + client/src/store/home.module.js | 29 ++++++ server/models/Task.js | 3 + server/package.json | 2 +- server/routes/api/tasks.js | 59 ++++++++++++ server/tests/api-tests.postman.json | 136 ++++++++++++++++++++++++++++ 8 files changed, 369 insertions(+), 52 deletions(-) create mode 100644 client/src/components/Task.vue diff --git a/client/src/components/Task.vue b/client/src/components/Task.vue new file mode 100644 index 0000000..a0497d1 --- /dev/null +++ b/client/src/components/Task.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/client/src/components/TaskList.vue b/client/src/components/TaskList.vue index 34616d3..ca60024 100644 --- a/client/src/components/TaskList.vue +++ b/client/src/components/TaskList.vue @@ -8,35 +8,7 @@ @@ -45,20 +17,17 @@ \ No newline at end of file diff --git a/client/src/store/actions.type b/client/src/store/actions.type index dfef241..0f1fc2f 100644 --- a/client/src/store/actions.type +++ b/client/src/store/actions.type @@ -6,3 +6,5 @@ export const A_TASK_RETRIEVE = "actionTaskRetrieve"; export const A_TASK_CREATE = "actionTaskCreate"; export const A_TASK_UPDATE = "actionTaskUpdate"; export const A_TASK_DELETE = "actionTaskDelete"; +export const A_TASK_TIMER_START = "actionTaskTimerStart"; +export const A_TASK_TIMER_STOP = "actionTaskTimerStop"; diff --git a/client/src/store/home.module.js b/client/src/store/home.module.js index 595d8db..9baaeab 100644 --- a/client/src/store/home.module.js +++ b/client/src/store/home.module.js @@ -4,6 +4,8 @@ import { A_TASK_DELETE, A_TASK_RETRIEVE, A_TASK_UPDATE, + A_TASK_TIMER_START, + A_TASK_TIMER_STOP, } from './actions.type' import { M_ERROR_SET, @@ -57,6 +59,30 @@ const actions = { }) }) }, + [A_TASK_TIMER_START](context, task) { + return new Promise(resolve => { + ApiService.put(`tasks/${task._id}/start`, task) + .then(({ data }) => { + context.commit(M_TASK_UPDATE, data) + resolve(data) + }) + .catch(({ response }) => { + context.commit(M_ERROR_SET, response.data.errors) + }) + }) + }, + [A_TASK_TIMER_STOP](context, task) { + return new Promise(resolve => { + ApiService.put(`tasks/${task._id}/stop`, task) + .then(({ data }) => { + context.commit(M_TASK_UPDATE, data) + resolve(data) + }) + .catch(({ response }) => { + context.commit(M_ERROR_SET, response.data.errors) + }) + }) + }, [A_TASK_DELETE](context, task) { return new Promise(resolve => { ApiService.delete(`tasks/${task._id}`, task) @@ -90,6 +116,9 @@ const mutations = { task.complete = data.complete task.description = data.description + task.timerActive = data.timerActive + task.timerStartedAt = data.timerStartedAt + task.timerTrackedTime = data.timerTrackedTime return task }) }, diff --git a/server/models/Task.js b/server/models/Task.js index 679d514..5f438ca 100644 --- a/server/models/Task.js +++ b/server/models/Task.js @@ -5,6 +5,9 @@ var TaskSchema = new mongoose.Schema( author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true }, description: { type: String, required: [true, "can't be blank"] }, complete: { type: Boolean, default: false }, + timerActive: { type: Boolean, default: false }, + timerStartedAt: { type: mongoose.Schema.Types.Date }, + timerTrackedTime: { type: mongoose.Schema.Types.Number, default: 0 }, }, { timestamps: true }, ) diff --git a/server/package.json b/server/package.json index 1f0e341..b8d717a 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "mongo:start": "docker run --name realworld-mongo -p 27017:27017 mongo & sleep 5", "start": "node ./app.js", "dev": "nodemon ./app.js", - "test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json --export-environment ./tests/env-api-tests.postman.json", + "test": "mongo < bin/wipe_db.js; newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json --export-environment ./tests/env-api-tests.postman.json", "stop": "lsof -ti :3000 | xargs kill", "mongo:stop": "docker stop realworld-mongo && docker rm realworld-mongo" }, diff --git a/server/routes/api/tasks.js b/server/routes/api/tasks.js index 8f8bb49..ae92a25 100644 --- a/server/routes/api/tasks.js +++ b/server/routes/api/tasks.js @@ -82,6 +82,65 @@ router.put('/:task', auth.required, function (req, res, next) { .catch(next) }) +router.put('/:task/start', auth.required, function (req, res, next) { + User.findById(req.payload.id) + .then(function (user) { + if (!user) { + return res.sendStatus(401) + } + + // authorized if user is author of task + if (req.task.author._id.toString() === req.payload.id.toString()) { + if (!req.task.timerActive) { + req.task.timerActive = true + req.task.timerStartedAt = Date.now() + } + + req.task + .save() + .then(function (task) { + return res.json(task) + }) + .catch(next) + } else { + return res.sendStatus(401) + } + }) + .catch(next) +}) + +router.put('/:task/stop', auth.required, function (req, res, next) { + User.findById(req.payload.id) + .then(function (user) { + if (!user) { + return res.sendStatus(401) + } + + // authorized if user is author of task + if (req.task.author._id.toString() === req.payload.id.toString()) { + if (req.task.timerActive) { + numSeconds = parseInt((Date.now() - req.task.timerStartedAt) / 1000) + + console.log(numSeconds) + + req.task.timerActive = false + req.task.timerTrackedTime += numSeconds + req.task.timerStartedAt = null + } + + req.task + .save() + .then(function (task) { + return res.json(task) + }) + .catch(next) + } else { + return res.sendStatus(401) + } + }) + .catch(next) +}) + // delete a task router.delete('/:task', auth.required, function (req, res, next) { User.findById(req.payload.id).then(function (user) { diff --git a/server/tests/api-tests.postman.json b/server/tests/api-tests.postman.json index 398b2c6..b20d6df 100644 --- a/server/tests/api-tests.postman.json +++ b/server/tests/api-tests.postman.json @@ -423,6 +423,142 @@ }, "response": [] }, + { + "name": "Start Timer", + "event": [ + { + "listen": "test", + "script": { + "id": "de4b00fb-eeb7-4da7-bd68-9e4d0981c162", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "tests['Response contains \"timerActive\" property'] = responseJSON.hasOwnProperty('timerActive');", + "", + "tests['Response contains \"timerStartedAt\" property'] = responseJSON.hasOwnProperty('timerStartedAt');", + "", + "tests['Response contains \"timerTrackedTime\" property'] = responseJSON.hasOwnProperty('timerTrackedTime');", + "", + "tests['timerTrackedTime attribute defaults to 0'] = responseJSON.timerTrackedTime == 0;", + "", + "tests['timerActive attribute is true'] = responseJSON.timerActive == true;", + "", + "tests['timerStartedAt attribute is not null'] = responseJSON.timerStartedAt != null;" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "X-Requested-With", + "type": "text", + "value": "XMLHttpRequest" + } + ], + "url": { + "raw": "{{apiUrl}}/tasks/:id/start", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "tasks", + ":id", + "start" + ], + "variable": [ + { + "key": "id", + "value": "{{taskId1}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Stop Timer", + "event": [ + { + "listen": "test", + "script": { + "id": "9601f63a-6ed0-4e51-8507-6760a05e61f3", + "exec": [ + "var responseJSON = JSON.parse(responseBody);", + "tests['Response contains \"timerActive\" property'] = responseJSON.hasOwnProperty('timerActive');", + "", + "tests['Response contains \"timerStartedAt\" property'] = responseJSON.hasOwnProperty('timerStartedAt');", + "", + "tests['Response contains \"timerTrackedTime\" property'] = responseJSON.hasOwnProperty('timerTrackedTime');", + "", + "tests['timerActive attribute is false'] = responseJSON.timerActive == false;", + "", + "tests['timerStartedAt attribute is null'] = responseJSON.timerStartedAt == null;" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "X-Requested-With", + "type": "text", + "value": "XMLHttpRequest" + } + ], + "url": { + "raw": "{{apiUrl}}/tasks/:id/stop", + "host": [ + "{{apiUrl}}" + ], + "path": [ + "tasks", + ":id", + "stop" + ], + "variable": [ + { + "key": "id", + "value": "{{taskId1}}" + } + ] + } + }, + "response": [] + }, { "name": "Create Task Empty Description", "event": [ From daaee316c1be6608ee2ffb9bb88b42dd7026ca45 Mon Sep 17 00:00:00 2001 From: oliveryh Date: Fri, 7 Aug 2020 22:58:05 +0100 Subject: [PATCH 02/13] :sparkles: split task lists into date categories over a given week (close #8) - add date picker to select week commencing date - create 7 columns for tasks lists to fall into - keep task creation text field tied to particular date in column --- client/src/components/Task.vue | 91 ++++++++++++++++++----------- client/src/components/TaskList.vue | 61 +++++++++++-------- client/src/components/TaskPanel.vue | 85 +++++++++++++++++++++++++++ client/src/store/home.module.js | 6 +- client/src/views/Home.vue | 6 +- server/models/Task.js | 4 ++ 6 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 client/src/components/TaskPanel.vue diff --git a/client/src/components/Task.vue b/client/src/components/Task.vue index a0497d1..28ce6ef 100644 --- a/client/src/components/Task.vue +++ b/client/src/components/Task.vue @@ -1,39 +1,55 @@ diff --git a/client/src/components/TaskList.vue b/client/src/components/TaskList.vue index ca60024..62f1f35 100644 --- a/client/src/components/TaskList.vue +++ b/client/src/components/TaskList.vue @@ -1,22 +1,26 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/client/src/components/TaskPanel.vue b/client/src/components/TaskPanel.vue new file mode 100644 index 0000000..c256eab --- /dev/null +++ b/client/src/components/TaskPanel.vue @@ -0,0 +1,85 @@ + + + + + \ No newline at end of file diff --git a/client/src/store/home.module.js b/client/src/store/home.module.js index 9baaeab..2b5403a 100644 --- a/client/src/store/home.module.js +++ b/client/src/store/home.module.js @@ -33,11 +33,9 @@ const actions = { }) }) }, - [A_TASK_CREATE](context, description) { + [A_TASK_CREATE](context, task) { return new Promise(resolve => { - ApiService.post('tasks', { - description: description, - }) + ApiService.post('tasks', task) .then(({ data }) => { context.commit(M_TASK_CREATE, data) resolve(data) diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 812a05f..c7f9a90 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,6 +1,6 @@