Search
⌘K
    to navigateEnterto select Escto close

    Assigning tasks to user

    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. User dropdown

    • The task details page should show the name of its assignee. Assignee in task details

    • 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 CreateTask and EditTask components and pass it to TaskForm through props.

    • We can add a new dropdown component using the library react-select in TaskForm 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 ShowTask 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 TableRow 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:

    1class UsersController < ApplicationController
    2  before_action :authenticate_user_using_x_auth_token, only: %i[index]
    3
    4  def index
    5    users = User.select(:id, :name)
    6    render status: :ok, json: { users: users }
    7  end
    8end

    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.

    You can also use the select method as the Array#select method in Ruby like so:

    1users = []
    2User.all.select do |user|
    3  users << { id: user.id, name: user.name }
    4end

    In the above code, User.all returns an array of user objects and then the Array#select method iterates over them to get the desired result. This approach however does not make much sense in this case as it makes the code relatively complicated than the first approach and adds extra steps to get the same result which can be avoided.

    To learn more about how the select method works, you can refer to the official documentation.

    Now we need to update routes.rb:

    1Rails.application.routes.draw do
    2  resources :tasks, except: %i[new edit], param: :slug
    3  resources :users, only: :index
    4
    5  root 'home#index'
    6  get '*path', to: 'home#index', via: :all
    7end

    Creating users API connector

    Let's create a new file to define all the APIs related to user model:

    1touch app/javascript/src/apis/users.js

    Now let's add the following code to it:

    1import axios from "axios";
    2
    3const list = () => axios.get("/users");
    4
    5const usersApi = {
    6  list,
    7};
    8
    9export default usersApi;

    Updating TaskForm component

    We are going to use react-select library rather than writing the select component from the scratch:

    1yarn add react-select

    Next, replace the whole content of the TaskForm component with the following lines:

    1import React from "react";
    2
    3import Button from "components/Button";
    4import Input from "components/Input";
    5import Select from "react-select";
    6
    7const TaskForm = ({
    8  type = "create",
    9  title,
    10  setTitle,
    11  assignedUser,
    12  users,
    13  setUserId,
    14  loading,
    15  handleSubmit,
    16}) => {
    17  const userOptions = users.map(user => ({
    18    value: user.id,
    19    label: user.name,
    20  }));
    21  const defaultOption = {
    22    value: assignedUser?.id,
    23    label: assignedUser?.name,
    24  };
    25
    26  return (
    27    <form className="max-w-lg mx-auto" onSubmit={handleSubmit}>
    28      <Input
    29        label="Title"
    30        placeholder="Todo Title (Max 50 Characters Allowed)"
    31        value={title}
    32        onChange={e => setTitle(e.target.value.slice(0, 50))}
    33      />
    34      <div className="flex flex-row items-center justify-start mt-3">
    35        <p className="w-3/12 leading-5 text-gray-800 text-md">Assigned To: </p>
    36        <div className="w-full">
    37          <Select
    38            options={userOptions}
    39            defaultValue={defaultOption}
    40            onChange={e => setUserId(e.value)}
    41            isSearchable
    42          />
    43        </div>
    44      </div>
    45      <Button
    46        type="submit"
    47        buttonText={type === "create" ? "Create Task" : "Update Task"}
    48        loading={loading}
    49      />
    50    </form>
    51  );
    52};
    53
    54export default TaskForm;

    Here, we are receiving users and assignedUser from props of TaskForm.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 TaskForm component, there we are formatting out the users and assignedUser, to the format required by react-select.

    Updating CreateTask component

    Now while creating a task we will assign a user to that task.

    Let's update app/javascript/src/components/Tasks/CreateTask.jsx and invoke the TaskForm component. We will be invoking the users index API from here and the result will be passed to TaskForm:

    1import React, { useState, useEffect } from "react";
    2import Container from "components/Container";
    3import TaskForm from "./Form/TaskForm";
    4import PageLoader from "components/PageLoader";
    5import tasksApi from "apis/tasks";
    6import usersApi from "apis/users";
    7
    8const CreateTask = ({ history }) => {
    9  const [title, setTitle] = useState("");
    10  const [userId, setUserId] = useState("");
    11  const [users, setUsers] = useState([]);
    12  const [loading, setLoading] = useState(false);
    13  const [pageLoading, setPageLoading] = useState(true);
    14
    15  const handleSubmit = async event => {
    16    event.preventDefault();
    17    setLoading(true);
    18    try {
    19      await tasksApi.create({ task: { title, assigned_user_id: userId } });
    20      setLoading(false);
    21      history.push("/dashboard");
    22    } catch (error) {
    23      logger.error(error);
    24      setLoading(false);
    25    }
    26  };
    27
    28  const fetchUserDetails = async () => {
    29    try {
    30      const response = await usersApi.list();
    31      setUsers(response.data.users);
    32      setUserId(response.data.users[0].id);
    33      setPageLoading(false);
    34    } catch (error) {
    35      logger.error(error);
    36      setPageLoading(false);
    37    }
    38  };
    39
    40  useEffect(() => {
    41    fetchUserDetails();
    42  }, []);
    43
    44  if (pageLoading) {
    45    return <PageLoader />;
    46  }
    47
    48  return (
    49    <Container>
    50      <TaskForm
    51        setTitle={setTitle}
    52        setUserId={setUserId}
    53        assignedUser={users[0]}
    54        loading={loading}
    55        handleSubmit={handleSubmit}
    56        users={users}
    57      />
    58    </Container>
    59  );
    60};
    61
    62export default CreateTask;

    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:

    1class TasksController < ApplicationController
    2  before_action :load_task, only: %i[show update destroy]
    3
    4  def index
    5    tasks = Task.all
    6    render status: :ok, json: { tasks: tasks }
    7  end
    8
    9  def create
    10    task = Task.new(task_params)
    11    if task.save
    12      render status: :ok, json: { notice: 'Task was successfully created' }
    13    else
    14      error = task.errors.full_messages.to_sentence
    15      render status: :unprocessable_entity, json: { error: error  }
    16    end
    17  end
    18
    19  def show
    20    render status: :ok, json: { task: @task, assigned_user: @task.assigned_user }
    21  end
    22
    23  def update
    24    if @task.update(task_params)
    25      render status: :ok, json: { notice: 'Successfully updated task.' }
    26    else
    27      render status: :unprocessable_entity,
    28        json: { error: @task.errors.full_messages.to_sentence }
    29    end
    30  end
    31
    32  def destroy
    33    if @task.destroy
    34      render status: :ok, json: { notice: 'Successfully deleted task.' }
    35    else
    36      render status: :unprocessable_entity,
    37        json: { error: @task.errors.full_messages.to_sentence }
    38    end
    39  end
    40
    41  private
    42
    43    def task_params
    44      params.require(:task).permit(:title, :assigned_user_id)
    45    end
    46
    47    def load_task
    48      @task = Task.find_by(slug: params[:slug])
    49      unless @task
    50        render status: :not_found, json: { error: t('task.not_found') }
    51      end
    52    end
    53end

    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. 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 EditTask component

    Open app/javascript/src/components/Tasks/EditTask.jsx and replace the entire content in it with the following lines of code:

    1import React, { useState, useEffect } from "react";
    2
    3import tasksApi from "apis/tasks";
    4import usersApi from "apis/users";
    5import Container from "components/Container";
    6import PageLoader from "components/PageLoader";
    7import { useParams } from "react-router-dom";
    8
    9import TaskForm from "./Form/TaskForm";
    10
    11const EditTask = ({ history }) => {
    12  const [title, setTitle] = useState("");
    13  const [userId, setUserId] = useState("");
    14  const [assignedUser, setAssignedUser] = useState("");
    15  const [users, setUsers] = useState([]);
    16  const [loading, setLoading] = useState(false);
    17  const [pageLoading, setPageLoading] = useState(true);
    18  const { slug } = useParams();
    19
    20  const handleSubmit = async event => {
    21    event.preventDefault();
    22    try {
    23      await tasksApi.update({
    24        slug,
    25        payload: { task: { title, assigned_user_id: userId } },
    26      });
    27      setLoading(false);
    28      history.push("/dashboard");
    29    } catch (error) {
    30      setLoading(false);
    31      logger.error(error);
    32    }
    33  };
    34
    35  const fetchUserDetails = async () => {
    36    try {
    37      const response = await usersApi.list();
    38      setUsers(response.data.users);
    39    } catch (error) {
    40      logger.error(error);
    41    } finally {
    42      setPageLoading(false);
    43    }
    44  };
    45
    46  const fetchTaskDetails = async () => {
    47    try {
    48      const response = await tasksApi.show(slug);
    49      setTitle(response.data.task.title);
    50      setAssignedUser(response.data.assigned_user);
    51      setUserId(response.data.assigned_user.id);
    52    } catch (error) {
    53      logger.error(error);
    54    }
    55  };
    56
    57  const loadData = async () => {
    58    await fetchTaskDetails();
    59    await fetchUserDetails();
    60  };
    61
    62  useEffect(() => {
    63    loadData();
    64  }, []);
    65
    66  if (pageLoading) {
    67    return (
    68      <div className="w-screen h-screen">
    69        <PageLoader />
    70      </div>
    71    );
    72  }
    73
    74  return (
    75    <Container>
    76      <TaskForm
    77        type="update"
    78        title={title}
    79        users={users}
    80        assignedUser={assignedUser}
    81        setTitle={setTitle}
    82        setUserId={setUserId}
    83        loading={loading}
    84        handleSubmit={handleSubmit}
    85      />
    86    </Container>
    87  );
    88};
    89
    90export default EditTask;

    When we click on the edit button on the task listing page we are redirected to the edit task page.

    Showing user names in ShowTask component

    Now we will display the user that is assigned to the task on task show page.

    Fully replace ShowTask.jsx with the following lines of code:

    1import React, { useState, useEffect } from "react";
    2import { useParams, useHistory } from "react-router-dom";
    3
    4import Container from "components/Container";
    5import PageLoader from "components/PageLoader";
    6import tasksApi from "apis/tasks";
    7
    8const ShowTask = () => {
    9  const { slug } = useParams();
    10  const [taskDetails, setTaskDetails] = useState([]);
    11  const [assignedUser, setAssignedUser] = useState([]);
    12  const [pageLoading, setPageLoading] = useState(true);
    13
    14  const history = useHistory();
    15
    16  const updateTask = () => {
    17    history.push(`/tasks/${taskDetails.slug}/edit`);
    18  };
    19
    20  const fetchTaskDetails = async () => {
    21    try {
    22      const response = await tasksApi.show(slug);
    23      setTaskDetails(response.data.task);
    24      setAssignedUser(response.data.assigned_user);
    25    } catch (error) {
    26      logger.error(error);
    27    } finally {
    28      setPageLoading(false);
    29    }
    30  };
    31
    32  useEffect(() => {
    33    fetchTaskDetails();
    34  }, []);
    35
    36  if (pageLoading) {
    37    return <PageLoader />;
    38  }
    39
    40  return (
    41    <Container>
    42      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
    43        <span className="text-gray-600">Task Title : </span>{" "}
    44        {taskDetails?.title}
    45      </h1>
    46      <div className="bg-bb-env px-2 mt-2 mb-4 rounded">
    47        <i
    48          className="text-2xl text-center transition cursor-pointer duration-300ease-in-out ri-edit-line hover:text-bb-yellow"
    49          onClick={updateTask}
    50        ></i>
    51      </div>
    52      <h2 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
    53        <span className="text-gray-600">Assigned To : </span>
    54        {assignedUser?.name}
    55      </h2>
    56    </Container>
    57  );
    58};
    59
    60export default ShowTask;

    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 ShowTask component. 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:

    1class TasksController
    2  # previous code
    3
    4  def index
    5    tasks = Task.all.as_json(include: { assigned_user: { only: %i[name id] } })
    6    render status: :ok, json: { tasks: tasks }
    7  end
    8
    9  # previous code
    10end

    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 TableRow component like so:

    1import React from "react";
    2import PropTypes from "prop-types";
    3
    4import Tooltip from "components/Tooltip";
    5
    6const TableRow = ({ data, destroyTask, showTask }) => {
    7  return (
    8    <tbody className="bg-white divide-y divide-gray-200">
    9      {data.map(rowData => (
    10        <tr key={rowData.id}>
    11          <td
    12            className="block w-64 px-6 py-4 text-sm font-medium
    13            leading-8 text-bb-purple capitalize truncate"
    14          >
    15            <Tooltip content={rowData.title} delay={200} direction="top">
    16              <div className="truncate max-w-64 ">{rowData.title}</div>
    17            </Tooltip>
    18          </td>
    19          <td
    20            className="px-6 py-4 text-sm font-medium
    21            leading-5 text-gray-900 whitespace-no-wrap"
    22          >
    23            {rowData.assigned_user.name}
    24          </td>
    25          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
    26            <a
    27              className="text-bb-purple"
    28              onClick={() => showTask(rowData.slug)}
    29            >
    30              Show
    31            </a>
    32          </td>
    33          <td
    34            className="px-6 py-4 text-sm font-medium
    35            leading-5 text-right cursor-pointer"
    36          >
    37            <a
    38              className="text-red-500
    39              hover:text-red-700"
    40              onClick={() => destroyTask(rowData.slug)}
    41            >
    42              Delete
    43            </a>
    44          </td>
    45        </tr>
    46      ))}
    47    </tbody>
    48  );
    49};
    50
    51TableRow.propTypes = {
    52  data: PropTypes.array.isRequired,
    53  destroyTask: PropTypes.func,
    54  showTask: PropTypes.func,
    55};
    56
    57export default TableRow;

    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:

    1user_name = tasks[0]["assigned_user"]["name"]

    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:

    1git add -A
    2git commit -m "Added ability to assign task to a user"
    Previous
    Next