From 91e05bf085ad8368a002f06769e4eea94afeab2f Mon Sep 17 00:00:00 2001 From: "Mr. Senko" Date: Wed, 7 Oct 2020 16:36:59 +0300 Subject: [PATCH] Implement drag & reorder functionality & new API NOTES: - uses html5sortable for drag & reorder - manual sorting button is located after A-Z button in toolbar - NEW API method TestPlan.update_case_order() - Manual reorder & API require the testcases.change_testcaseplan permission - toolbarDropdowns() is called from within toolbarEvents() b/c it really doesn't make sense to have this as stand-alone function - toolbarEvents() is moved within the TestCases.filter() callback b/c we need the list of test cases to be fully initialized so we can get the initial sorting order and use it later WARNINGS: - may want to unbind the on-click event for rows if users are having problems with the rows expanding while dragging them --- tcms/package.json | 1 + tcms/rpc/api/testplan.py | 21 +++++++++++ tcms/testplans/static/testplans/js/get.js | 41 +++++++++++++++++++-- tcms/testplans/templates/testplans/get.html | 15 +++++++- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/tcms/package.json b/tcms/package.json index b66dee6879..70d3ed737d 100644 --- a/tcms/package.json +++ b/tcms/package.json @@ -1,6 +1,7 @@ { "dependencies": { "bootstrap-switch": "3.3.4", + "html5sortable": "0.9.18", "marked": "1.2.0", "patternfly": "3.59.5", "prismjs": "1.21.0", diff --git a/tcms/rpc/api/testplan.py b/tcms/rpc/api/testplan.py index eb31c51732..5541255ba0 100644 --- a/tcms/rpc/api/testplan.py +++ b/tcms/rpc/api/testplan.py @@ -17,6 +17,7 @@ 'add_case', 'remove_case', + 'update_case_order', 'add_tag', 'remove_tag', @@ -202,6 +203,26 @@ def remove_case(plan_id, case_id): TestCasePlan.objects.filter(case=case_id, plan=plan_id).delete() +@permissions_required('testcases.change_testcaseplan') +@rpc_method(name='TestPlan.update_case_order') +def update_case_order(plan_id, case_id, sortkey): + """ + .. function:: RPC TestPlan.update_case_order(plan_id, case_id, sortkey) + + Update sortkey which controls display order of the given test case in + the given test plan. + + :param plan_id: PK of TestPlan holding the selected TestCase + :type plan_id: int + :param case_id: PK of TestCase to be modified + :type case_id: int + :param sortkey: Ordering value, e.g. 10, 20, 30 + :type sortkey: int + :raises PermissionDenied: if missing *testcases.delete_testcaseplan* permission + """ + TestCasePlan.objects.filter(case=case_id, plan=plan_id).update(sortkey=sortkey) + + @permissions_required('attachments.view_attachment') @rpc_method(name='TestPlan.list_attachments') def list_attachments(plan_id, **kwargs): diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index b7d337dfa3..3d44d090f5 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -24,13 +24,14 @@ $(document).ready(function() { drawTestCases(allTestCases, testPlanId, permissions); treeViewBind(); - // b/c treeViewBind() will modfy handlers/visibility for both + // b/c treeViewBind() will modify handlers/visibility for both // test plan family tree and the test cases tree adjustTestPlanFamilyTree(); - }); - toolbarDropdowns(); - toolbarEvents(testPlanId, permissions); + // b/c drag & reorder needs the initial order of test cases and + // they may not be fully loaded when Sortable() is initialized! + toolbarEvents(testPlanId, permissions); + }); collapseDocumentText(); }); @@ -278,6 +279,7 @@ function attachEvents(testCases, testPlanId, permissions) { } function toolbarEvents(testPlanId, permissions) { + toolbarDropdowns(); $('.js-checkbox-toolbar').click(function(ev) { const isChecked = ev.target.checked; @@ -320,6 +322,37 @@ function toolbarEvents(testPlanId, permissions) { sortTestCases(); }); + + // always initialize the sortable list however you can only + // move items using the handle icon on the left which becomes + // visible only when the manual sorting button is clicked + sortable('#testcases-list', { + handle: '.handle', + itemSerializer: (serializedItem, sortableContainer) => { + return parseInt(serializedItem.node.getAttribute('data-testcase-pk')) + } + }); + + // IMPORTANT: this is not empty b/c sortable() is initialized *after* + // all of the test cases have been rendered !!! + const initialOrder = sortable('#testcases-list', 'serialize')[0].items; + + $('.js-toolbar-manual-sort').click(function(event) { + $(this).blur(); + $('.js-toolbar-manual-sort').find('span').toggleClass(['fa-sort', 'fa-check-square']); + $('.js-testcase-sort-handler, .js-testcase-expand-arrow, .js-testcase-checkbox').toggleClass('hidden'); + + const currentOrder = sortable('#testcases-list', 'serialize')[0].items; + + // rows have been rearranged and the results must be committed to the DB + if (currentOrder.join() !== initialOrder.join()) { + currentOrder.forEach(function(tc_pk, index) { + jsonRPC('TestPlan.update_case_order', [testPlanId, tc_pk, index*10], function(result) {}); + }); + } + }); + + $('.js-toolbar-priority').click(function(ev) { let selectedCases = getSelectedTestCases(); diff --git a/tcms/testplans/templates/testplans/get.html b/tcms/testplans/templates/testplans/get.html index 48b80507b8..a25670d44a 100644 --- a/tcms/testplans/templates/testplans/get.html +++ b/tcms/testplans/templates/testplans/get.html @@ -134,6 +134,12 @@

+ + {% if perms.testcases.change_testcaseplan %} + + {% endif %}
@@ -195,6 +201,7 @@

{% if perms.testcases.change_testcase %}
  • {% trans 'Reviewer' %}
  • {% endif %} +

    @@ -240,10 +247,13 @@

    -
    + +
    -
    +
    @@ -359,6 +369,7 @@

  • {% trans 'Reviewer' %}
  • +