Features
We will be adding a feature to mark a task as "complete".
These are the expected changes:
-
Tasks will have two possible states: pending or completed. A task can be in any one of these states at a time. Let us call this state the progress of the task from now on.
-
Progress of every task will be pending by default when created.
-
Pending and completed tasks should be listed separately. Since completed tasks need not require any special attention, they can be listed at bottom of the page in dim colors.
-
On the tasks list page, there should be a checkbox for every task to switch their progress. Checked means the task is completed and unchecked means pending.
-
When the progress of a task is changed, it should immediately move to its designated list. In other words, a completed task should not stay in the pending task's list once its checkbox is checked.
-
No notification should be displayed if a task's progress is successfully updated because from a UI perspective the result of this action will be visible and showing a notification is not required.
-
The task delete button should only be visible for completed tasks.
-
A delete button should be present in the show task page which should allow users to delete pending tasks.
-
User should be redirected to the dashboard page from the show task page if task is successfully deleted.
-
The assignee name need not be shown for completed tasks.
-
Both task assignee and task owner should be allowed to update the task's progress.
Our tasks list page will look like this when the feature implementation is complete:
Technical design
Implementation of this feature can be done like so:
-
We will add an enum attribute called progress in the Task model. The value of this enum attribute will be a string.
-
We will add a new string column named progress in the tasks table and use ActiveRecord::Enum to map the enum attributes with the string values stored in the progress column.
-
In the index action of the task controller, we will filter pending and completed tasks separately and will return them in the response.
-
We will add a Jbuilder for index action in TasksController to declare and render a JSON of completed and pending tasks.
-
Since we will reuse update action to change the progress of tasks, we need a finer control over the authorization now. That is, only the creator should be allowed to change the title and assignee of the task. But both creator and assignee should be allowed to change its progress.
-
For controlling authorization, we will create a before_action callback which checks who is updating which field and throws an error in case the operation is not allowed.
-
Towards the bottom of our tasks list page, we will add a new table to display the list of completed tasks.
-
We will conditionally render the delete button and assignee name based on the progress of the task.
-
To add more flexibility, we will also add the ability for the user to delete a task from the ShowTask component too. We will create a deleteTask function to delete the task even if the task is pending.
Now let us start implementing the feature.
Storing progress in DB
Let's create a migration to add a column called progress to Tasks table:
1bundle exec rails g migration AddProgressToTasks
Open db/migrate/add_progress_to_tasks.rb file and add following lines:
1class AddProgressToTasks < ActiveRecord::Migration[6.1] 2 def change 3 add_column :tasks, :progress, :string, default: "pending", null: false 4 end 5end
Now let's run the migration:
1bundle exec rails db:migrate
Since the progress attribute will always consist of a predefined set of values, which are pending and completed, we can make use of an enum data type.
We will be making use of ActiveRecord::Enum for the same. It allows us to declare an enum attribute where the values map to strings in the database, but can be queried by name.
Now let's define an enum in models/tasks.rb. Add the following line:
1class Task < ApplicationRecord 2 enum progress: { pending: "pending", completed: "completed" } 3 4 # previous code 5 6 private 7 8 # previous code 9end
As discussed in the previous chapter, ActiveRecord::Enum adds a lot of instances methods to the enum variable, which will help us easily retrieve data. For example:
1task.completed? # => true 2task.progress # => "completed"
To know more about the usage of enums, you can refer to the Using Enums in Rails chapter and come back.
By default all the tasks will be marked as pending, since we have already set the default value for the progress column as pending in the migration that we wrote.
To update the progress of a task, we will be making use of the already existing update action in tasks_controller.rb. However we need to refactor the update action so that no success message is sent along with the response if the request is for updating a task's progress. To do so we will attach a quiet query parameter in the request URL for a progress update request. If the param is present then a success message should be sent.
As we already know, we should first permit the parameter progress in task_params method.
Let's update the task_params method as shown below:
1def task_params 2 params.require(:task).permit(:title, :assigned_user_id, :progress) 3end
Now update the update action of TasksController like so:
1def update 2 authorize @task 3 @task.update!(task_params) 4 respond_with_success(t("successfully_updated", entity: "Task")) unless params.key?(:quiet) 5end
Now let's update the index action of TasksController to send both pending and completed tasks separately in the response as shown below:
1def index 2 tasks = policy_scope(Task) 3 @pending_tasks = tasks.pending.as_json(include: { assigned_user: { only: %i[name id] } }) 4 @completed_tasks = tasks.completed 5end
So far we have been using the as_json method to load the association data. The problem with using as_json to load assigned_user here is that for each pending_task a new query will be sent to the database to load its associated assigned_user.
This is called an n+1 query issue where n number of additional queries are made to fetch some data that could be fetched in one query. To learn more about the n+1 query issue and how to avoid it you can refer to the in-depth n+1 queries and memoization chapter.
We can avoid n+1 queries by using the includes method to fetch the assigned_user like so:
1def index 2 tasks = policy_scope(Task) 3 @pending_tasks = tasks.pending.includes(:assigned_user) 4 @completed_tasks = tasks.completed 5end
Specifying the association name in includes method will preload the association data for all pending tasks and no additional queries would be required to get the assigned_user details. To read about the includes method and how it works in-depth refer to the n+1 queries and memoization chapter.
Now let's add a Jbuilder for index action to declare a JSON response structure for tasks.
Run the following command to create the index.json.jbuilder file:
1touch ./app/views/tasks/index.json.jbuilder
Add the following lines of code inside the app/views/tasks/index.json.jbuilder file:
1json.tasks do 2 json.pending @pending_tasks do |pending_task| 3 json.id pending_task.id 4 json.title pending_task.title 5 json.slug pending_task.slug 6 json.progress pending_task.progress 7 json.status pending_task.status 8 json.assigned_user do 9 json.extract! pending_task.assigned_user, 10 :id, 11 :name 12 end 13 end 14 15 json.completed @completed_tasks 16end
Finally we need to update our task factory to include a value for progress. Add the following line to test/factories/task.rb:
1# frozen_string_literal: true 2 3FactoryBot.define do 4 factory :task do 5 association :assigned_user, factory: :user 6 association :task_owner, factory: :user 7 title { Faker::Lorem.sentence[0..49] } 8 progress {'pending'} 9 end 10end
Partials in Jbuilder
Attributes like id, title, slug and assigned_user in the tasks.pending JSON are also present in the JSON structure we had declared inside the show.json.jbuilder. Instead of writing the same code again we should declare a partial JSON structure which will contain the common attributes of a task object and then we can import that partial JSON in index.json.jbuilder and show.json.jbuilder.
Jbuilder lets us declare partial JSONs. To declare a partial JSON structure for task, create a new file called _task.json.jbuilder using the following command:
1touch app/views/tasks/_task.json.jbuilder
Name of a partial JSON file is always prefixed with an underscore and it should match with the model name. In this case, the partial JSON will render a JSON of task attributes hence it is named after the Task model.
Add the following lines of code inside the _task.json.jbuilder file:
1json.id task.id 2json.title task.title 3json.slug task.slug 4json.assigned_user do 5 json.extract! task.assigned_user, 6 :id, 7 :name 8end
We have added the common attributes from show.json.jbuilder and index.json.jbuilder in the partial _task.json.jbuilder file. Now import the partial JSON file inside show.json.jbuilder. To do so, fully update the show.json.jbuilder with the following lines of code:
1json.task do 2 json.partial! "tasks/task", task: @task 3 4 json.comments @comments do |comment| 5 json.extract! comment, 6 :id, 7 :content, 8 :created_at 9 end 10 11 json.task_owner do 12 json.extract! @task.task_owner, 13 :name 14 end 15end
Similarly, fully update the index.json.jbuilder like so:
1json.tasks do 2 json.pending @pending_tasks do |pending_task| 3 json.partial! "tasks/task", task: pending_task 4 json.extract! pending_task, 5 :progress, 6 :status 7 end 8 9 json.completed @completed_tasks 10end
Jbuilder provides a partial! method which accepts the relative path of the partial Jbuilder file and a variable which contains the object whose attributes are to be used to build the JSON. This variable can be accessed inside the partial Jbuilder file.
In this case, the relative path of the partial Jbuilder file is tasks/task and the variable is task which contains the @task object. task can be accessed inside the partial _task.json.jbuilder file. To learn more about the partial! method you can refer to the official documentation for Jbuilder.
Fine tuning authorization
Now, we need to allow both creator and assignee to update the progress of tasks. More importantly, we need to disallow the assignee from updating any other fields and permit only the creator.
Here in this case, the title, task_owner_id and assigned_user_id should be restricted attributes. That is, only the creator of the task should be allowed to modify their values.
Conversely, progress attribute will be unrestricted. Both the creator and assignee should be able to change its value.
We have two options in this situation:
-
Instead of using task_params method to filter the params, use pundit's strong parameters style. This way, we will conditionally filter the request data from pundit policy class before accessing it from the update action. We will remove everything except the :progress parameter if the request is from the assignee.
-
The plus point of this approach is that it delegates all the authorization-related tasks to the pundit policy itself. The controller will remain lean.
-
The problem with this approach is its unpredictability. It silently ignores the disallowed params and executes the update action with the remaining ones. It won't throw an error even if all supplied params were filtered out.
-
So, from the user's point of view, saving changes would seem to be successful since no error message is shown. But the task's attributes won't change. The user might suspect this as a bug. Ideally, we should send a forbidden status if user is trying to modify unauthorized parameters.
-
-
The second way is to add a before_action callback for update action. From there, if the request is from the assignee and if the request params contains any restricted attributes (title or user_id), throw authorization error.
-
The advantages and disadvantages of this approach are the opposite of the first method.
-
Advantage is that we will be able to handle the response based on the task params and send back an appropriate message in case of authorization errors.
-
Disadvantage is that we need to code the authorization logic in our controller itself.
-
For the first method, its drawbacks overshadow its advantages. Therefore, let us proceed with the second approach.
First, we need to grant access to update action for both assignee and creator in our pundit policy. Open app/policies/task_policy.rb and update the following lines:
1 # ... previous code ... 2 def show? 3 task.task_owner_id == user.id || task.assigned_user_id == user.id 4 end 5 6 # The condition for edit policy is the same as that of the show. 7 # Hence, we can simply call `show?` inside the edit? policy here. 8 def edit? 9 show? 10 end 11 12 # Similar in the case for update? policy. 13 def update? 14 show? 15 end 16 # ... previous code ...
Now, open app/models/task.rb and define the attributes that are restricted for assignee:
1class Task < ApplicationRecord 2 RESTRICTED_ATTRIBUTES = %i[title task_owner_id assigned_user_id] 3 # ... previous code ... 4end
Now, in app/controllers/tasks_controller.rb, add the following lines:
1class TasksController < ApplicationController 2 after_action :verify_authorized, except: :index 3 after_action :verify_policy_scoped, only: :index 4 before_action :load_task!, only: %i[show update destroy] 5 before_action :ensure_authorized_update_to_restricted_attrs, only: :update 6 7 # ...previous code... 8 9 private 10 11 def task_params 12 params.require(:task).permit(:title, :assigned_user_id, :progress, :status) 13 end 14 15 def ensure_authorized_update_to_restricted_attrs 16 is_editing_restricted_params = Task::RESTRICTED_ATTRIBUTES.any? { |a| task_params.key?(a) } 17 is_not_owner = @task.task_owner_id != @current_user.id 18 if is_editing_restricted_params && is_not_owner 19 handle_authorization_error 20 end 21 end 22 23 # ...previous code...
The beauty of writing clean and fluent code is that there isn't a requirement for another person to explain what the method does. People will be able to figure it out on their own because the naming is very accurate. It's almost like reading an English sentence!
The overall gist of the above method is that if the request is not from the owner of the task and if it is trying to edit the restricted attributes, then we will raise handle_authorization_error before the action update is even executed.
Since the request encounters an error in the before_action callback, the update action won't run. The error message will be returned as the response.
Adding toggle for pending/completed tasks
Let us now move on to the frontend. We need to make a new list to show completed tasks and we need to create a toggle mechanism in the UI to let a user toggle the progress of a task as completed or pending.
Fully replace the content of app/javascript/src/components/Dashboard/index.jsx, with the following code:
1import React, { useState, useEffect } from "react"; 2import { all, isNil, isEmpty, either } from "ramda"; 3import tasksApi from "apis/tasks"; 4import Container from "components/Container"; 5import PageLoader from "components/PageLoader"; 6import Table from "components/Tasks/Table/index"; 7 8const Dashboard = ({ history }) => { 9 const [pendingTasks, setPendingTasks] = useState([]); 10 const [completedTasks, setCompletedTasks] = useState([]); 11 const [loading, setLoading] = useState(true); 12 13 const fetchTasks = async () => { 14 try { 15 const response = await tasksApi.list(); 16 setPendingTasks(response.data.tasks.pending); 17 setCompletedTasks(response.data.tasks.completed); 18 } catch (error) { 19 logger.error(error); 20 } finally { 21 setLoading(false); 22 } 23 }; 24 25 const destroyTask = async slug => { 26 try { 27 await tasksApi.destroy(slug); 28 await fetchTasks(); 29 } catch (error) { 30 logger.error(error); 31 } 32 }; 33 34 const handleProgressToggle = async ({ slug, progress }) => { 35 try { 36 await tasksApi.update({ 37 slug, 38 payload: { task: { progress } }, 39 quiet: true, 40 }); 41 await fetchTasks(); 42 } catch (error) { 43 logger.error(error); 44 } finally { 45 setLoading(false); 46 } 47 }; 48 49 const showTask = slug => { 50 history.push(`/tasks/${slug}/show`); 51 }; 52 53 useEffect(() => { 54 fetchTasks(); 55 }, []); 56 57 if (loading) { 58 return ( 59 <div className="w-screen h-screen"> 60 <PageLoader /> 61 </div> 62 ); 63 } 64 65 if (all(either(isNil, isEmpty), [pendingTasks, completedTasks])) { 66 return ( 67 <Container> 68 <h1 className="my-5 text-xl leading-5 text-center"> 69 You have not created or been assigned any tasks 🥳 70 </h1> 71 </Container> 72 ); 73 } 74 75 return ( 76 <Container> 77 {!either(isNil, isEmpty)(pendingTasks) && ( 78 <Table 79 data={pendingTasks} 80 destroyTask={destroyTask} 81 showTask={showTask} 82 handleProgressToggle={handleProgressToggle} 83 /> 84 )} 85 {!either(isNil, isEmpty)(completedTasks) && ( 86 <Table 87 type="completed" 88 data={completedTasks} 89 destroyTask={destroyTask} 90 handleProgressToggle={handleProgressToggle} 91 /> 92 )} 93 </Container> 94 ); 95}; 96 97export default Dashboard;
Now update the apis/tasks.js API connector file with the quiet argument like so:
1// previous code 2 3const update = ({ slug, payload, quiet = false }) => { 4 const path = quiet ? `/tasks/${slug}?quiet` : `/tasks/${slug}`; 5 return axios.put(path, payload); 6};
If you recall earlier in this chapter we discussed about attaching a quiet query param to the URL in case we don't want the response to contain a success message.
We haven't passed a value to the quiet query parameter because logically it can be either true or false. Passing a boolean will be redundant in this case. We can infer the value in backend by checking for the presence of the quiet key in the params hash. If the key is present then the value should be true otherwise false.
Moreover, URLs are strings so a boolean query param would be typecasted to a string value i.e. true would be typecasted to "true". In the server side, we would have had to add additional logic to convert the value of the quiet param to a boolean.
When a task is updated from the EditTask page, a success message will be sent from the backend and it will be displayed as a notification. The API call for updating a task from the EditTask component doesn't contain a quiet argument as shown in the following code snippet:
1const handleSubmit = async event => { 2 event.preventDefault(); 3 try { 4 await tasksApi.update({ 5 slug, 6 payload: { 7 task: { title, assigned_user_id: userId }, 8 }, 9 }); 10 setLoading(false); 11 history.push("/"); 12 } catch (error) { 13 setLoading(false); 14 logger.error(error); 15 } 16};
The URL in this case would be /tasks/${slug}. It doesn't contain a quiet param therefore a success message will be sent in the response.
Fully replace the content of Table/index.jsxwith the following lines of code:
1import React from "react"; 2import TableHeader from "./TableHeader"; 3import TableRow from "./TableRow"; 4 5const Table = ({ 6 type = "pending", 7 data, 8 destroyTask, 9 showTask, 10 handleProgressToggle, 11 starTask, 12}) => { 13 return ( 14 <div className="flex flex-col mt-10 "> 15 <div className="my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> 16 <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> 17 <div className="overflow-hidden border-b border-gray-200 shadow md:custom-box-shadow"> 18 <table className="min-w-full divide-y divide-gray-200"> 19 <TableHeader type={type} /> 20 <TableRow 21 data={data} 22 destroyTask={destroyTask} 23 showTask={showTask} 24 type={type} 25 handleProgressToggle={handleProgressToggle} 26 starTask={starTask} 27 /> 28 </table> 29 </div> 30 </div> 31 </div> 32 </div> 33 ); 34}; 35 36export default Table;
Here, we are passing type and handleProgressToggle as props to TableRow. The type denotes the current progress, that is pending or completed.
Now, inside TableHeader.jsx, fully replace with the following content:
1import React from "react"; 2import { compose, head, join, juxt, tail, toUpper } from "ramda"; 3 4const TableHeader = ({ type }) => { 5 const getTitleCase = compose(join(""), juxt([compose(toUpper, head), tail])); 6 7 const title = `${getTitleCase(type)} Tasks`; 8 9 return ( 10 <thead> 11 <tr> 12 <th className="w-1"></th> 13 <th 14 className="px-6 py-3 text-xs font-bold 15 leading-4 tracking-wider text-left text-bb-gray-600 16 text-opacity-50 uppercase bg-gray-50" 17 > 18 {title} 19 </th> 20 {type === "pending" && ( 21 <th 22 className="px-6 py-3 text-sm font-bold leading-4 23 tracking-wider text-left text-bb-gray-600 24 text-opacity-50 bg-gray-50" 25 > 26 Assigned To 27 </th> 28 )} 29 {type === "completed" && ( 30 <> 31 <th style={{ width: "164px" }}></th> 32 <th 33 className="pl-6 py-3 text-sm font-bold leading-4 34 tracking-wider text-center text-bb-gray-600 35 text-opacity-50 bg-gray-50" 36 > 37 Delete 38 </th> 39 </> 40 )} 41 {type === "pending" && ( 42 <th 43 className="pl-6 py-3 text-sm font-bold leading-4 44 tracking-wider text-center text-bb-gray-600 45 text-opacity-50 bg-gray-50" 46 > 47 Starred 48 </th> 49 )} 50 </tr> 51 </thead> 52 ); 53}; 54 55export default TableHeader;
In the TableHeader, we are conditionally rendering table headers using the && operator. Such blocks will be rendered only if the condition preceding it is truthy.
But that approach has an edge case. If the condition preceding the JSX evaluates to 0, which is falsy, the JSX won't be rendered. But react will display 0 in the UI where we expected no JSX or block to be rendered.
Therefore be careful when using && to render conditional blocks when the condition statement is a function/variable that may not strictly be of type boolean.
Also, if the grouped statements are too long or too complex, then it ought to be moved out into a separate component within that file, so as to improve readability.
Now, inside TableRow.jsx, fully replace with the following content:
1import React from "react"; 2import classnames from "classnames"; 3import PropTypes from "prop-types"; 4 5import Tooltip from "components/Tooltip"; 6 7const TableRow = ({ 8 type = "pending", 9 data, 10 destroyTask, 11 showTask, 12 handleProgressToggle, 13}) => { 14 const isCompleted = type === "completed"; 15 const toggledProgress = isCompleted ? "pending" : "completed"; 16 17 return ( 18 <tbody className="bg-white divide-y divide-bb-gray-600"> 19 {data.map(rowData => ( 20 <tr key={rowData.id}> 21 <td className="px-6 py-4 text-center"> 22 <input 23 type="checkbox" 24 checked={isCompleted} 25 className="ml-6 w-4 h-4 text-bb-purple border-gray-300 26 rounded form-checkbox focus:ring-bb-purple cursor-pointer" 27 onChange={() => 28 handleProgressToggle({ 29 slug: rowData.slug, 30 progress: toggledProgress, 31 }) 32 } 33 /> 34 </td> 35 <td 36 className={classnames( 37 "block w-64 px-6 py-4 text-sm font-medium leading-8 text-bb-purple capitalize truncate", 38 { 39 "cursor-pointer": !isCompleted, 40 }, 41 { "text-opacity-50": isCompleted } 42 )} 43 onClick={() => !isCompleted && showTask(rowData.slug)} 44 > 45 <Tooltip content={rowData.title} delay={200} direction="top"> 46 <div className="truncate max-w-64 ">{rowData.title}</div> 47 </Tooltip> 48 </td> 49 {!isCompleted && ( 50 <td 51 className="px-6 py-4 text-sm font-medium leading-5 52 text-bb-gray-600 whitespace-no-wrap" 53 > 54 {rowData.assigned_user.name} 55 </td> 56 )} 57 {isCompleted && ( 58 <> 59 <td style={{ width: "164px" }}></td> 60 <td className="px-6 py-4 text-center cursor-pointer"> 61 <i 62 className="text-2xl text-center text-bb-border 63 transition duration-300 ease-in-out 64 ri-delete-bin-5-line hover:text-bb-red" 65 onClick={() => destroyTask(rowData.slug)} 66 ></i> 67 </td> 68 </> 69 )} 70 </tr> 71 ))} 72 </tbody> 73 ); 74}; 75 76TableRow.propTypes = { 77 data: PropTypes.array.isRequired, 78 type: PropTypes.string, 79 destroyTask: PropTypes.func, 80 showTask: PropTypes.func, 81 handleProgressToggle: PropTypes.func, 82}; 83 84export default TableRow;
After making the above mentioned changes, we will be able to see two tables in the dashboard. One shows the list of completed tasks and other one shows the list of pending tasks.
The handleProgressToggle function allows a user to toggle the progress of a task between completed/pending.
A user can toggle between the two states of the task by clicking on the input checkbox part of each TableRow.
Previously we had added the ability for the user to delete completed tasks. But in order to be more flexible, we can add the same delete feature in our ShowTask component and allow deletion of even pending tasks.
Let us add another delete button inside the ShowTask component. This will allow the user to delete a task even if the task is pending.
Fully replace ShowTask.jsx with the following content:
1import React, { useEffect, useState } from "react"; 2import { useParams, useHistory } from "react-router-dom"; 3 4import tasksApi from "apis/tasks"; 5import commentsApi from "apis/comments"; 6import Container from "components/Container"; 7import PageLoader from "components/PageLoader"; 8import Comments from "components/Comments"; 9 10const ShowTask = () => { 11 const { slug } = useParams(); 12 const [task, setTask] = useState([]); 13 const [pageLoading, setPageLoading] = useState(true); 14 const [newComment, setNewComment] = useState(""); 15 const [loading, setLoading] = useState(false); 16 17 let history = useHistory(); 18 19 const destroyTask = async () => { 20 try { 21 await tasksApi.destroy({ slug: task.slug }); 22 } catch (error) { 23 logger.error(error); 24 } finally { 25 history.push("/"); 26 } 27 }; 28 29 const updateTask = () => { 30 history.push(`/tasks/${task.slug}/edit`); 31 }; 32 33 const fetchTaskDetails = async () => { 34 try { 35 const response = await tasksApi.show(slug); 36 setTask(response.data.task); 37 } catch (error) { 38 logger.error(error); 39 } finally { 40 setPageLoading(false); 41 } 42 }; 43 44 const handleSubmit = async event => { 45 event.preventDefault(); 46 try { 47 await commentsApi.create({ 48 comment: { content: newComment, task_id: task.id }, 49 }); 50 fetchTaskDetails(); 51 setNewComment(""); 52 setLoading(false); 53 } catch (error) { 54 logger.error(error); 55 setLoading(false); 56 } 57 }; 58 59 useEffect(() => { 60 fetchTaskDetails(); 61 }, []); 62 63 if (pageLoading) { 64 return <PageLoader />; 65 } 66 67 return ( 68 <Container> 69 <div className="flex justify-between text-bb-gray-600 mt-10"> 70 <h1 className="pb-3 mt-5 mb-3 text-lg leading-5 font-bold"> 71 {task?.title} 72 </h1> 73 <div className="bg-bb-env px-2 mt-2 mb-4 rounded"> 74 <i 75 className="text-2xl text-center transition duration-300 76 ease-in-out ri-delete-bin-5-line hover:text-bb-red mr-2" 77 onClick={destroyTask} 78 ></i> 79 <i 80 className="text-2xl text-center transition duration-300 81 ease-in-out ri-edit-line hover:text-bb-yellow" 82 onClick={updateTask} 83 ></i> 84 </div> 85 </div> 86 <h2 87 className="pb-3 mb-3 text-md leading-5 text-bb-gray-600 88 text-opacity-50" 89 > 90 <span>Assigned To : </span> 91 {task?.assigned_user.name} 92 </h2> 93 <h2 className="pb-3 mb-3 text-md leading-5 text-bb-gray-600 text-opacity-50"> 94 <span>Created By : </span> 95 {task?.task_owner?.name} 96 </h2> 97 <Comments 98 comments={task?.comments} 99 setNewComment={setNewComment} 100 handleSubmit={handleSubmit} 101 newComment={newComment} 102 loading={loading} 103 /> 104 </Container> 105 ); 106}; 107 108export default ShowTask;
Users can now delete pending tasks if they wish to on the show task page.
When the users delete the task from the show task page, they should be redirected back to the dashboard once the task is successfully deleted and a success notification should be displayed.
If you recall we didn't add the logic to send a success message in the response when a task is deleted from the dashboard.
We need to update the destroy action of the TasksController to send a success message in the JSON response only if the task is successfully deleted from the show task page. We can handle this in the same manner we handled the task progress update.
We should update the API connector for deleting a task and add a quiet query param to the task destroy API endpoint if the task is deleted from the dashboard.
Update the destroy action in the TasksController like so:
1def destroy 2 authorize @task 3 @task.destroy! 4 respond_with_success("Task was successfully deleted!") unless params.key?(:quiet) 5end
Now, update the following lines of code in the Dashboard component:
1// previous imports 2 3const Dashboard = ({ history }) => { 4 // previous code 5 6 const destroyTask = async slug => { 7 try { 8 await tasksApi.destroy({ slug, quiet: true })); 9 await fetchTasks(); 10 } catch (error) { 11 logger.error(error); 12 } 13 }; 14}; 15 16export default Dashboard;
Finally update the tasks API connected file like so:
1// previous code 2 3const destroy = ({ slug, quiet }) => { 4 const path = quiet ? `/tasks/${slug}?quiet` : `/tasks/${slug}`; 5 return axios.delete(path); 6};
Moving response messages to i18n en.locales
Let's move the response messages to en.yml:
1en: 2 successfully_created: "%{entity} was successfully created!" 3 successfully_updated: "%{entity} was successfully updated!" 4 successfully_deleted: "%{entity} was successfully deleted!" 5 task: 6 slug: 7 immutable: "is immutable!"
Let's update the destroy action of TasksController like so:
1def destroy 2 authorize @task 3 @task.destroy! 4 respond_with_success("successfully_deleted", entity: "Task") unless params.key?(:quiet) 5end
Now let's commit these changes:
1git add -A 2git commit -m "Added progress to tasks"