Tutorial: How to add table view of linked documents within form

Hi,
Ok so the default way of viewing linked documents is via connections. So lets say for instance I want to view the tasks list for a particular project, either i can go to the tasks list view and add a filter or access them via the connections tab in the project form(which more or less does the same as the former). Instead i wanted to add a separate tab in the project form containing a html field which show a paginated table of all tasks for a particular project. So I added an html field to the project doctype called “tasks_list” and added a client side script.

Here’s the script:

frappe.ui.form.on("Project", {
  refresh: function(frm) {
    console.log("Inside refresh function");

    if (frm.fields_dict['tasks_list']) {
      var cur_field = frm.fields_dict['tasks_list'];
      console.log("cur_field found:", cur_field);

      // Function to fetch tasks
      function fetch_tasks(start, limit) {
        console.log("Fetching tasks from start:", start, "with limit:", limit);
        frappe.call({
          method: "frappe.client.get_list",
          args: {
            doctype: "Task",
            filters: [["project", "=", frm.doc.name]],
            fields: ["name", "status", "description"],
            limit_start: start,
            limit_page_length: limit,
          },
          callback: function(response) {
            console.log("Response received:", response);
            if (!response.message || response.message.length === 0) {
              cur_field.html("No linked tasks found for this project.");
              return;
            }

            try {
              var task_list_html = get_task_list_html(response.message);
              cur_field.$wrapper.html(task_list_html); // Set the HTML
              add_pagination_controls(cur_field.$wrapper, start, limit, response.message.length);
            } catch (error) {
              console.error("Error generating HTML:", error);
              cur_field.$wrapper.html("Failed to display linked tasks.");
            }
          },
          error: function(error) {
            console.error("Error in frappe call:", error);
            cur_field.$wrapper.html("Failed to fetch linked tasks.");
          }
        });
      }

      // Initial fetch with page 1
      fetch_tasks(0, 10);
    } else {
      console.error("Custom field 'tasks_list' not found.");
    }
  },
});

// Function to generate HTML
function get_task_list_html(data) {
  try {
    var table_html = '<div class="table-responsive"><table class="table table-bordered">';
    table_html += "<thead><tr>";
    table_html += "<th>Name</th><th>Status</th><th>Description</th>"; // Add table headers
    table_html += "</tr></thead><tbody>";

    // Iterate through fetched tasks and build table rows
    data.forEach(task => {
      table_html += "<tr>";
      table_html += `<td>${task.name || ""}</td>`;
      table_html += `<td>${task.status || ""}</td>`;
      table_html += `<td>${task.description || ""}</td>`;
      table_html += "</tr>";
    });

    table_html += "</tbody></table></div>";
    return table_html;
  } catch (error) {
    console.error("Error in get_task_list_html function:", error);
    throw error;
  }
}

// Function to add pagination controls
function add_pagination_controls(container, start, limit, fetched_count) {
  try {
    container = $(container); // Ensure container is a jQuery object
    var pagination_html = '<div class="pagination-controls">';
    if (start > 0) {
      pagination_html += `<button class="btn btn-secondary" onclick="fetch_tasks(${start - limit}, ${limit})">Previous</button>`;
    }
    if (fetched_count === limit) {
      pagination_html += `<button class="btn btn-secondary" onclick="fetch_tasks(${start + limit}, ${limit})">Next</button>`;
    }
    pagination_html += '</div>';
    container.append(pagination_html);
  } catch (error) {
    console.error("Error in add_pagination_controls function:", error);
    throw error;
  }
}

// Make fetch_tasks globally accessible
window.fetch_tasks = function(start, limit) {
  var frm = cur_frm; // Use the global cur_frm object
  if (frm.fields_dict['tasks_list']) {
    var cur_field = frm.fields_dict['tasks_list'];
    console.log("Fetching tasks for pagination from start:", start, "with limit:", limit);
    frappe.call({
      method: "frappe.client.get_list",
      args: {
        doctype: "Task",
        filters: [["project", "=", frm.doc.name]],
        fields: ["name", "status", "description"],
        limit_start: start,
        limit_page_length: limit,
      },
      callback: function(response) {
        console.log("Pagination response received:", response);
        if (!response.message || response.message.length === 0) {
          cur_field.$wrapper.html("No linked tasks found for this project.");
          return;
        }

        try {
          var task_list_html = get_task_list_html(response.message);
          cur_field.$wrapper.html(task_list_html); // Set the HTML
          add_pagination_controls(cur_field.$wrapper, start, limit, response.message.length);
        } catch (error) {
          console.error("Error generating HTML for pagination:", error);
          cur_field.$wrapper.html("Failed to display linked tasks.");
        }
      },
      error: function(error) {
        console.error("Error in frappe call for pagination:", error);
        cur_field.$wrapper.html("Failed to fetch linked tasks.");
      }
    });
  } else {
    console.error("Custom field 'tasks_list' not found.");
  }
};

Here’s a screenshot of the result:

This lets you view all linked doctypes for another doctype within the form in a list view.
You can change the fields as per your wishes and dont forget to remove all the console error handlers.
Cheerio!

6 Likes

Nice @Vesper_Solutions,

You can easily set all project tasks in the table.

This can be done using a server script, which might be helpful to someone. (You can put the script in the server script doctype.)


When tasks are saved, they will automatically be added to the project.

Script Type: DocType Event
Reference Document Type: Task
DocType Event: After Save

if doc.project:
    project = frappe.get_doc('Project', doc.project)
    found = False

    for total_task in project.total_tasks:
        if total_task.task == doc.name:
            total_task.subject = doc.subject
            total_task.status = doc.status
            total_task.description = doc.description
            found = True
            break

    if not found:
        project.append('total_tasks', {
            'task': doc.name,
            'subject': doc.subject,
            'status': doc.status,
            'description': doc.description
        })
    
    project.save()

When a user deletes a task, it will also be automatically removed from the project.

Script Type: DocType Event
Reference Document Type: Task
DocType Event: Before Delete

if doc.project:
    project = frappe.get_doc('Project', doc.project)
    to_remove = None
    
    for total_task in project.total_tasks:
        if total_task.task == doc.name:
            to_remove = total_task
            break

    if to_remove:
        project.remove(to_remove)
        project.save()

Output:

1 Like

Of course. I just showing a proof of concept but your method is more comprehensive and allows more leeway. :ok_hand:

@Vesper_Solutions solution is my preference, since I cannot find solution from frappe to ONLY show other doctype data. But I prefer using frappe.datatable instead of coding html from scratch.