Features
In this chapter, we are going to see how to assign tasks to a user. The
following is the list of things we need in this feature:
-
Every task should have a single user as its assignee. A user can have zero or
more assigned tasks.
-
We need a new dropdown in the task form showing the list of users. We should
be able to select the task's assignee from this dropdown.
- Each task on the dashboard should contain the task assignee's name along with
the task's title.
Technical design
Let us break down the requirement into smaller sub-tasks and explain our way of
approach in a bit more technical way:
-
Our controller for the User
model will have an index
action that returns a
list of users. This is required to show the dropdown in the tasks form.
-
We use this API to retrieve the list of users from Create
and Edit
components and pass it to Form
through props
.
-
We can add a new dropdown component using the library react-select
in Form
component. This will be filled with the list of users it will receive through
its props
.
-
On submitting, the selected user's id
will be passed to the create/update
API of tasks. That value will be stored against the assigned_user_id
column.
-
We will update the show
action in tasks controller to include details of
task assignee. We can use that information to display the name of assignee in
the Show
component.
-
We will update the index
action of the TasksController
to include the
details of assigned_user
for each task in the JSON response. Then we will
update the Row
component to display the task assignee's name.
Creating index action for user
Let's create an new file app/controllers/users_controller.rb
and add the
following lines of code to it:
The select
method accepts a subset of fields and only returns the selected
fields from the result set. In this case, the select
method will fetch an
ActiveRecord::Relation
object containing the names and ids of all the users
and store it in the users
variable.
The SQL query for the above mentioned select
method will be like this:
We can also use the select
method as the Array#select
method in Ruby by
adding a block that returns a boolean response.
For example:
In the above code, User.all
returns an array of user objects and then the
Array#select
method iterates over them to check the given condition. If an
object returns true
for the given condition then that object will be selected
into the voting_candidates
array.
Although we can also get the required results by using the Array#select
method
in the above mentioned index
action, we shouldn't use it. That's because we
are not selecting some specific records or objects, but we are selecting
specific columns from all the objects. In this case, using Array#map
method is
a better approach.
For example:
However, this map
method approach does not make much sense in the index
action as it makes the code relatively more complicated than the first approach
and adds extra steps to get the same result which can be avoided.
So to conclude we should use ActiveRecord#select
method when we want to select
specific columns or properties from an array of objects or records and
Array#select
method when we want to select specific objects from an array of
objects. As in the index
action, we are selecting some specific columns from
an array of records so the correct approach would be like this:
To learn more about how the select
method works, you can refer to the
official documentation
from Rails.
Now we need to update routes.rb
:
Creating users API connector
Let's create a new file to define all the APIs related to user model:
Now let's add the following code to it:
Updating Form component
We are going to use react-select library rather
than writing the select
component from the scratch:
Next, replace the whole content of the Form
component with the following
lines:
Here, we are receiving users
and assignedUser
from props of Form.jsx
which
will be used to populate the Select
component with usernames and also as a
default value.
Note that, when we select an item from the usernames, the corresponding id
of
the user is what gets passed into setUserId
.
If you notice the first two lines in the Form
component, there we are
formatting out the users
and assignedUser
, to the format required by
react-select
.
Note that, in the above code, we have used a variable named isNotDirty
to disable the "Save changes" button. This ensures that the "Save changes" button is only enabled if the user has made any changes to the form values.
To ensure a better user experience, we should disable the primary button when the form is not dirty to prevent accidental submissions and unwanted requests.
Now, let's see how we have checked whether a form is dirty.
To check whether a form is dirty, we need to store the initial or default values of the form data and compare them with the current state of the form data. Since the initial form values should not change, even if the form re-renders, we made use of useRef
hook to store this value. The useRef
hook allows us to persist values between re-renders.
The useRef
hook accepts the initial value that we want to store. Here, we have passed an object that contains title
and assignedUser.id
as values to the useRef
hook:
It returns a reference to a mutable object, which doesn't trigger a re-render. We can access the stored value using the current
property of this object, i.e. initialValues.current
will return the object containing title
and userId
. This value will not change even if the title
and the assignedUser
changes due to re-rendering.
Now that we have the initialValues
set, we can compare it with an object containing the title
and assignedUser.id
values derived from prop
values during rendering like so:
We have used the equals
function from ramda
to compare the two objects by their values.
Updating Create component
Now while creating a task we will assign a user to that task.
Let's update app/javascript/src/components/Tasks/Create.jsx
and invoke the
Form
component. We will be invoking the users index
API from here and the
result will be passed to Form
:
After submitting the form we'll get task
as params with attribute
assigned_user_id
.
Updating TasksController
If we look into the SQL statements generated on the server, we see that
assigned_user_id
is not being used in the sql statements. That's because we
are not marking assigned_user_id
as a safe parameter. We need to change
task_params
to whitelist assigned_user_id
attribute.
We also need to update the show
action of TasksController
to respond with
the task assignee along with other task details.
If you recall we had added a belongs_to
association in the Task
model called
assigned_user
. When we declare an association inside a model, Rails creates a
method inside the model by the name of the association which returns the
associated object when invoked.
In this case, the method name will be assigned_user
and it will return the
task assignee when invoked. We can use the assigned_user
method to obtain the
task assignee inside the show action of TasksController.
If you are keen to know how this works under the hood, you can refer to the
Rails macros and metaprogramming chapter
in this book which deals with this topic in depth.
To introduce the required changes, update the TasksController
with the
following lines of code:
After making the changes we just discussed, assigned_user_id
will get stored
while creating a new task and we will get assigned_user
along with other task
details in the frontend upon fetching a task.
Start Rails sever and visit http://localhost:3000/dashboard. Clicking on the create task
button will redirect you to the page to create new task. Select a user from the
dropdown menu, add a title, and create the task. That's it.
Updating Edit component
Open app/javascript/src/components/Tasks/Edit.jsx
and replace the entire
content in it with the following lines of code:
In the Edit
component we want to wait till both the user
and task
details
are fetched. Thus we have used Promise.all
to wait for both the promises to
resolve in a parallel fashion. And after successfully fetching the details, we
set the pageLoading
state to false
.
When we click on the edit button on the task listing page we are redirected to
the edit task page.
Showing user names in Show component
Now we will display the user that is assigned to the task on task show page.
Fully replace Show.jsx
with the following lines of code:
Now, while clicking on the show button of a task in the Table
component which
is rendered in Dashboard
, we will be routed to the Show
component from
Tasks
folder. There we can see the task details, which would include the
title, as well as the assigned user's name.
In this chapter, we haven't added any tests for the User model because we don't
have much to test for.
Currently, the User model is at its barebones level. As we move further, we will
be adding some test cases for the User model.
Showing task assignee in Dashboard
To display the task assignee detail in the Dashboard we need to update the
index
action in TasksController
to respond with the assigned_user along with
other task details.
Update the index
action of the TasksController
as follows:
Don't get overwhelmed by the as_json
method in the above code. We will take a
brief look at it in the next section of this chapter.
So far we have only been showing the task title in each of the tasks listed on
the dashboard. To display task assignee's name along with the task title in
dashboard update the Row
component like this:
Using the as_json method
The as_json
method is a part of the ActiveModel::Serializers::JSON
module.
It returns a hash of attributes present in the object. The hash will only
consist of keys in string format rather than symbol format. Thus we'd have to
query using the string itself. Example:
We can invoke the as_json
method on an object without passing any arguments.
If we do not pass any arguments, then returned hash will include all the
object's attributes.
We are calling the as_json
method on a collection of task objects which will
return a hash of all attributes for each of the task objects.
We have also passed an include
option to the as_json
method. Passing
include
inside the as_json
method includes a nested hash of the object's
associations as a key in the hash. The key is named after the name of
association. In our case, we are including the assigned_user
association for
each of the task objects.
At last, we have passed the only
method that limits the attributes included in
the returned hash to the attributes which are passed in the only
method.
Note that only
has been nested inside the user association hash, hence it will
only limit the assigned_user's attributes and return the name
and id
of
associated assigned_user for each task.
Now let's commit these changes: