From 66cdd4e955167bfdcffac946a1d42b9e14f338df Mon Sep 17 00:00:00 2001 From: ohurst Date: Fri, 2 May 2025 22:45:44 +0000 Subject: [PATCH 1/8] testplan-multi-clone --- tcms/testplans/forms.py | 29 +++++++ tcms/testplans/static/testplans/js/search.js | 62 ++++++++++++++- .../templates/testplans/multi_clone.html | 77 +++++++++++++++++++ .../testplans/templates/testplans/search.html | 4 + tcms/testplans/urls.py | 1 + tcms/testplans/views.py | 63 +++++++++++++++ 6 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 tcms/testplans/templates/testplans/multi_clone.html diff --git a/tcms/testplans/forms.py b/tcms/testplans/forms.py index d3e817bc54..6532ae2c9e 100644 --- a/tcms/testplans/forms.py +++ b/tcms/testplans/forms.py @@ -82,3 +82,32 @@ def populate(self, product_pk): ) else: self.fields["version"].queryset = Version.objects.none() + +class CloneMultiPlanForm(forms.Form): # pylint: disable=must-inherit-from-model-form + plan = forms.ModelMultipleChoiceField( + queryset=TestPlan.objects.all(), + ) + + product = forms.ModelChoiceField( + queryset=Product.objects.all(), + empty_label=None, + required=False, + ) + version = forms.ModelChoiceField( + queryset=Version.objects.none(), + empty_label=None, + required=False, + ) + + copy_testcases = forms.BooleanField(required=False) + set_parent = forms.BooleanField(required=False) + + def populate(self, plans): + if plans: + product_pk = TestPlan.objects.filter(pk__in=plans[0])[0].product_id + self.fields["version"].queryset = Version.objects.filter( + product_id=product_pk + ) + self.fields["plan"].queryset = TestPlan.objects.filter(pk__in=plans) + else: + self.fields["version"].queryset = Version.objects.none() diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index 83a7eec96b..df2ef71aa4 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -93,6 +93,11 @@ export function pageTestplansSearchReadyHandler () { dataTableJsonRPC('TestPlan.filter', params, callbackF, preProcessData) }, columns: [ + { + data: null, + orderable: false, + render: function(){ return ''; } + }, { data: null, defaultContent: '', @@ -165,7 +170,22 @@ export function pageTestplansSearchReadyHandler () { processing: '
', zeroRecords: 'No records found' }, - order: [[1, 'asc']] + order: [[2, 'asc']] + }) + + // header “select all” + $('#resultsTable thead').on('change', '#select-all', function(){ + const checked = this.checked; + $('#resultsTable tbody input.row-select') + .prop('checked', checked) + .trigger('change'); + }) + + // row checkbox handler + $('#resultsTable tbody').on('change', 'input.row-select', function(){ + const $tr = $(this).closest('tr'); + if(this.checked) table.row($tr).select(); + else table.row($tr).deselect(); }) // Add event listener for opening and closing nested test plans @@ -189,9 +209,44 @@ export function pageTestplansSearchReadyHandler () { return false // so we don't actually send the form }) + $('#btn_clone_bulk').click(function () { + const selectedTestPlans = getSelectedTestPlans(); + if (selectedTestPlans.length === 0) { + alert('No test plans selected for cloning.'); + return false; + } + + window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) + }) + $('#id_product').change(updateVersionSelectFromProduct) } +function getSelectedTestPlans () { + const inputs = $('#resultsTable tbody input.row-select:checked').closest('tr') + const tpIds = [] + + inputs.each(function (_, el) { + const id = $(el).closest('tr').find('td:nth-child(3)').text().trim(); + if (id) { + tpIds.push(id); + } + + // Check if the row has collapsed children and add their IDs + const row = $('#resultsTable').DataTable().row($(el).closest('tr')); + if (hiddenChildRows[id] && !row.child.isShown()) { + hiddenChildRows[id].forEach(function (childRow) { + const childId = $(childRow).find('td:nth-child(3)').text().trim(); + if (childId) { + tpIds.push(childId); + } + }); + } + }) + + return tpIds +} + function hideExpandedChildren (table, parentRow) { const children = hiddenChildRows[parentRow.data().id] children.forEach( @@ -214,5 +269,10 @@ function renderChildrenOf (parentRow, data) { // this is an array of previously hidden rows const children = hiddenChildRows[data.id] $(children).find('td').css('border', '0').css('padding-left', `${childPadding}px`) + + if($(parentRow).find('input.row-select').prop('checked')) { + $(children).find('input.row-select').prop('checked', true).trigger('change') + } + return $(children).show() } diff --git a/tcms/testplans/templates/testplans/multi_clone.html b/tcms/testplans/templates/testplans/multi_clone.html new file mode 100644 index 0000000000..322366225a --- /dev/null +++ b/tcms/testplans/templates/testplans/multi_clone.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Clone TestPlan" %} - {{ test_plan.name }}{% endblock %} +{% block page_id %}page-testplans-mutable{% endblock %} + +{% block contents %} +
+
+ {% csrf_token %} + + {% for plan in form.plan.field.queryset %} + + {% endfor %} + +
+
+ + + +
+
+ + {{ form.product.errors }} +
+ +
+ + + + + +
+
+ + {{ form.version.errors }} +
+
+ +
+ +
+ +

{% trans "Clone or link existing TCs into new TP" %}

+
+ + + +
+ +

{% trans "Set the source TP as parent of new TP" %}

+
+
+ +
+
+ +
+
+
+ +
+{% endblock %} diff --git a/tcms/testplans/templates/testplans/search.html b/tcms/testplans/templates/testplans/search.html index 6b23fb43e2..89e8b83613 100644 --- a/tcms/testplans/templates/testplans/search.html +++ b/tcms/testplans/templates/testplans/search.html @@ -102,6 +102,9 @@
+
+ +
@@ -110,6 +113,7 @@ + diff --git a/tcms/testplans/urls.py b/tcms/testplans/urls.py index a70c4bd225..99143b183f 100644 --- a/tcms/testplans/urls.py +++ b/tcms/testplans/urls.py @@ -18,6 +18,7 @@ ), re_path(r"^(?P\d+)/edit/$", views.Edit.as_view(), name="plan-edit"), re_path(r"^(?P\d+)/clone/$", views.Clone.as_view(), name="plans-clone"), + re_path(r"^clone/$", views.MultiClone.as_view(), name="plans-multi-clone"), re_path(r"^search/$", views.SearchTestPlanView.as_view(), name="plans-search"), re_path(r"^new/$", views.NewTestPlanView.as_view(), name="plans-new"), ] diff --git a/tcms/testplans/views.py b/tcms/testplans/views.py index 3cc217f5f9..2864b882f5 100644 --- a/tcms/testplans/views.py +++ b/tcms/testplans/views.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- +from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect from django.urls import reverse +from django.shortcuts import get_object_or_404, render from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView @@ -16,6 +18,7 @@ from tcms.testcases.models import TestCaseStatus from tcms.testplans.forms import ( ClonePlanForm, + CloneMultiPlanForm, NewPlanForm, PlanNotifyFormSet, SearchPlanForm, @@ -226,3 +229,63 @@ def form_valid(self, form): return HttpResponseRedirect( reverse("test_plan_url_short", args=[cloned_plan.pk]) ) + +@method_decorator(permission_required("testplans.add_testplan"), name="dispatch") +class MultiClone(FormView): + template_name = "testplans/multi_clone.html" + form_class = CloneMultiPlanForm + + http_method_names = ["get", "post"] + + def get(self, request): + if not self._is_request_data_valid(request, "p"): + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + get_params = request.GET.copy() + get_params.setlist("plan", request.GET.getlist("p")) + del get_params["p"] + + planids = get_params.getlist("plan") + + clone_form = CloneMultiPlanForm(get_params) + clone_form.populate(plans=planids) + + context = { + "form": clone_form, + } + return render(request, self.template_name, context) + + def post(self, request): + if not self._is_request_data_valid(request): + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + # Do the clone action + clone_form = CloneMultiPlanForm(request.POST) + clone_form.populate(plans=request.POST.getlist("plan")) + + if clone_form.is_valid(): + + for tp_src in clone_form.cleaned_data["plan"]: + tp_dest = tp_src.clone(name=tp_src.name, **clone_form.cleaned_data) + + # Otherwise tell the user the clone action is successful + messages.add_message( + request, messages.SUCCESS, _("TestPlan cloning was successful") + ) + return HttpResponseRedirect(reverse("plans-search")) + + # invalid form + messages.add_message(request, messages.ERROR, clone_form.errors) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + @staticmethod + def _is_request_data_valid(request, field_name="plan"): + request_data = getattr(request, request.method) + + if field_name not in request_data: + messages.add_message( + request, messages.ERROR, _("At least one TestPlan is required") + ) + return False + + return True From 647e5288ab15f76900b92ada0cfdff8051ada51a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 22:55:52 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcms/testplans/static/testplans/js/search.js | 2 +- tcms/testplans/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index df2ef71aa4..5ce1ace2e0 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -273,6 +273,6 @@ function renderChildrenOf (parentRow, data) { if($(parentRow).find('input.row-select').prop('checked')) { $(children).find('input.row-select').prop('checked', true).trigger('change') } - + return $(children).show() } diff --git a/tcms/testplans/views.py b/tcms/testplans/views.py index 2864b882f5..e5fdbe4366 100644 --- a/tcms/testplans/views.py +++ b/tcms/testplans/views.py @@ -277,7 +277,7 @@ def post(self, request): # invalid form messages.add_message(request, messages.ERROR, clone_form.errors) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) - + @staticmethod def _is_request_data_valid(request, field_name="plan"): request_data = getattr(request, request.method) From b20d701550d36aad5471803a7524eb8dccc971ff Mon Sep 17 00:00:00 2001 From: ohurst Date: Sat, 3 May 2025 00:30:04 +0000 Subject: [PATCH 3/8] formatting fixes --- tcms/testplans/forms.py | 1 + tcms/testplans/static/testplans/js/search.js | 38 ++++++++++---------- tcms/testplans/views.py | 11 +++--- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/tcms/testplans/forms.py b/tcms/testplans/forms.py index 6532ae2c9e..df69a3a74e 100644 --- a/tcms/testplans/forms.py +++ b/tcms/testplans/forms.py @@ -83,6 +83,7 @@ def populate(self, product_pk): else: self.fields["version"].queryset = Version.objects.none() + class CloneMultiPlanForm(forms.Form): # pylint: disable=must-inherit-from-model-form plan = forms.ModelMultipleChoiceField( queryset=TestPlan.objects.all(), diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index 5ce1ace2e0..4698bfbc40 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -96,7 +96,7 @@ export function pageTestplansSearchReadyHandler () { { data: null, orderable: false, - render: function(){ return ''; } + render: function () { return '' } }, { data: null, @@ -174,18 +174,18 @@ export function pageTestplansSearchReadyHandler () { }) // header “select all” - $('#resultsTable thead').on('change', '#select-all', function(){ - const checked = this.checked; + $('#resultsTable thead').on('change', '#select-all', function () { + const checked = this.checked $('#resultsTable tbody input.row-select') - .prop('checked', checked) - .trigger('change'); + .prop('checked', checked) + .trigger('change') }) // row checkbox handler - $('#resultsTable tbody').on('change', 'input.row-select', function(){ - const $tr = $(this).closest('tr'); - if(this.checked) table.row($tr).select(); - else table.row($tr).deselect(); + $('#resultsTable tbody').on('change', 'input.row-select', function () { + const $tr = $(this).closest('tr') + if (this.checked) table.row($tr).select() + else table.row($tr).deselect() }) // Add event listener for opening and closing nested test plans @@ -210,10 +210,10 @@ export function pageTestplansSearchReadyHandler () { }) $('#btn_clone_bulk').click(function () { - const selectedTestPlans = getSelectedTestPlans(); + const selectedTestPlans = getSelectedTestPlans() if (selectedTestPlans.length === 0) { - alert('No test plans selected for cloning.'); - return false; + alert('No test plans selected for cloning.') + return false } window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) @@ -227,20 +227,20 @@ function getSelectedTestPlans () { const tpIds = [] inputs.each(function (_, el) { - const id = $(el).closest('tr').find('td:nth-child(3)').text().trim(); + const id = $(el).closest('tr').find('td:nth-child(3)').text().trim() if (id) { - tpIds.push(id); + tpIds.push(id) } // Check if the row has collapsed children and add their IDs - const row = $('#resultsTable').DataTable().row($(el).closest('tr')); + const row = $('#resultsTable').DataTable().row($(el).closest('tr')) if (hiddenChildRows[id] && !row.child.isShown()) { hiddenChildRows[id].forEach(function (childRow) { - const childId = $(childRow).find('td:nth-child(3)').text().trim(); + const childId = $(childRow).find('td:nth-child(3)').text().trim() if (childId) { - tpIds.push(childId); + tpIds.push(childId) } - }); + }) } }) @@ -270,7 +270,7 @@ function renderChildrenOf (parentRow, data) { const children = hiddenChildRows[data.id] $(children).find('td').css('border', '0').css('padding-left', `${childPadding}px`) - if($(parentRow).find('input.row-select').prop('checked')) { + if ($(parentRow).find('input.row-select').prop('checked')) { $(children).find('input.row-select').prop('checked', true).trigger('change') } diff --git a/tcms/testplans/views.py b/tcms/testplans/views.py index e5fdbe4366..28925ffb7a 100644 --- a/tcms/testplans/views.py +++ b/tcms/testplans/views.py @@ -3,8 +3,8 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect +from django.shortcuts import render from django.urls import reverse -from django.shortcuts import get_object_or_404, render from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView @@ -17,8 +17,8 @@ from tcms.management.models import Priority from tcms.testcases.models import TestCaseStatus from tcms.testplans.forms import ( - ClonePlanForm, CloneMultiPlanForm, + ClonePlanForm, NewPlanForm, PlanNotifyFormSet, SearchPlanForm, @@ -230,6 +230,7 @@ def form_valid(self, form): reverse("test_plan_url_short", args=[cloned_plan.pk]) ) + @method_decorator(permission_required("testplans.add_testplan"), name="dispatch") class MultiClone(FormView): template_name = "testplans/multi_clone.html" @@ -237,7 +238,7 @@ class MultiClone(FormView): http_method_names = ["get", "post"] - def get(self, request): + def get(self, request): # pylint: disable=W0221 if not self._is_request_data_valid(request, "p"): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) @@ -255,7 +256,7 @@ def get(self, request): } return render(request, self.template_name, context) - def post(self, request): + def post(self, request): # pylint: disable=W0221 if not self._is_request_data_valid(request): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) @@ -266,7 +267,7 @@ def post(self, request): if clone_form.is_valid(): for tp_src in clone_form.cleaned_data["plan"]: - tp_dest = tp_src.clone(name=tp_src.name, **clone_form.cleaned_data) + tp_src.clone(name=tp_src.name, **clone_form.cleaned_data) # Otherwise tell the user the clone action is successful messages.add_message( From 8caa4c1df1ecb88a28a679093868144857a260f8 Mon Sep 17 00:00:00 2001 From: ohurst Date: Sat, 3 May 2025 00:34:55 +0000 Subject: [PATCH 4/8] space fix --- tcms/testplans/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tcms/testplans/views.py b/tcms/testplans/views.py index 28925ffb7a..ef5a96d98d 100644 --- a/tcms/testplans/views.py +++ b/tcms/testplans/views.py @@ -238,7 +238,7 @@ class MultiClone(FormView): http_method_names = ["get", "post"] - def get(self, request): # pylint: disable=W0221 + def get(self, request): # pylint: disable=W0221 if not self._is_request_data_valid(request, "p"): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) @@ -256,7 +256,7 @@ def get(self, request): # pylint: disable=W0221 } return render(request, self.template_name, context) - def post(self, request): # pylint: disable=W0221 + def post(self, request): # pylint: disable=W0221 if not self._is_request_data_valid(request): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) From 43a668ef3e5c79e0f36329a09d2f14819ba4d60a Mon Sep 17 00:00:00 2001 From: ohurst Date: Mon, 5 May 2025 19:57:32 +0000 Subject: [PATCH 5/8] update gui --- tcms/testplans/static/testplans/js/search.js | 53 ++++++++++++------- .../testplans/templates/testplans/search.html | 6 +-- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index 4698bfbc40..cf4447ce5c 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -48,6 +48,20 @@ export function pageTestplansSearchReadyHandler () { initializeDateTimePicker('#id_before') initializeDateTimePicker('#id_after') + const multiCloneButton = { + text: '', + titleAttr: 'Clone Selected', + action: function(e, dt, node, config){ + const selectedTestPlans = getSelectedTestPlans() + if (selectedTestPlans.length === 0) { + alert($('#main-element').data('trans-no-testplans-selected')) + return false + } + + window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) + } + }; + const rowsNotShownMessage = $('#main-element').data('trans-some-rows-not-shown') const table = $('#resultsTable').DataTable({ pageLength: $('#navbar').data('defaultpagesize'), @@ -164,21 +178,30 @@ export function pageTestplansSearchReadyHandler () { }) }, dom: 'Bptp', - buttons: exportButtons, + buttons: [ + multiCloneButton, + ...exportButtons + ], language: { loadingRecords: '
', processing: '
', zeroRecords: 'No records found' }, - order: [[2, 'asc']] - }) - - // header “select all” - $('#resultsTable thead').on('change', '#select-all', function () { - const checked = this.checked - $('#resultsTable tbody input.row-select') - .prop('checked', checked) - .trigger('change') + order: [[2, 'asc']], + initComplete: function(){ + const btnContainer = table.buttons().container(); + $(btnContainer).prepend( + '' + ); + + // Hook the checkbox change event to “select all” + $('#onlyActive').on('change', function(){ + const checked = this.checked + $('#resultsTable tbody input.row-select') + .prop('checked', checked) + .trigger('change') + }); + } }) // row checkbox handler @@ -209,16 +232,6 @@ export function pageTestplansSearchReadyHandler () { return false // so we don't actually send the form }) - $('#btn_clone_bulk').click(function () { - const selectedTestPlans = getSelectedTestPlans() - if (selectedTestPlans.length === 0) { - alert('No test plans selected for cloning.') - return false - } - - window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) - }) - $('#id_product').change(updateVersionSelectFromProduct) } diff --git a/tcms/testplans/templates/testplans/search.html b/tcms/testplans/templates/testplans/search.html index 89e8b83613..2264adbaca 100644 --- a/tcms/testplans/templates/testplans/search.html +++ b/tcms/testplans/templates/testplans/search.html @@ -10,6 +10,7 @@ class="container-fluid container-cards-pf" id="main-element" data-trans-some-rows-not-shown="{% trans 'Some child test plans do not match search criteria'%}" + data-trans-no-testplans-selected="{% trans 'No rows selected! Please select at least one!'%}" >
{% csrf_token %} @@ -102,9 +103,6 @@
-
- -
@@ -113,7 +111,7 @@
{% trans "ID" %} {% trans "Test plan" %}
- + From c3d34cc7c80161113c9512e59e96ae4f73448719 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 19:58:09 +0000 Subject: [PATCH 6/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcms/testplans/static/testplans/js/search.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index cf4447ce5c..9d6763f687 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -57,7 +57,7 @@ export function pageTestplansSearchReadyHandler () { alert($('#main-element').data('trans-no-testplans-selected')) return false } - + window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) } }; @@ -193,7 +193,7 @@ export function pageTestplansSearchReadyHandler () { $(btnContainer).prepend( '' ); - + // Hook the checkbox change event to “select all” $('#onlyActive').on('change', function(){ const checked = this.checked From daf49deb9dd3b09bae491151513b638684b0afbf Mon Sep 17 00:00:00 2001 From: ohurst Date: Mon, 5 May 2025 23:59:03 +0000 Subject: [PATCH 7/8] recursive child check --- tcms/testplans/static/testplans/js/search.js | 53 +++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index 9d6763f687..a7c3cd1b46 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -51,7 +51,7 @@ export function pageTestplansSearchReadyHandler () { const multiCloneButton = { text: '', titleAttr: 'Clone Selected', - action: function(e, dt, node, config){ + action: function (e, dt, node, config) { const selectedTestPlans = getSelectedTestPlans() if (selectedTestPlans.length === 0) { alert($('#main-element').data('trans-no-testplans-selected')) @@ -60,7 +60,7 @@ export function pageTestplansSearchReadyHandler () { window.location.assign(`/plan/clone/?p=${selectedTestPlans.join('&p=')}`) } - }; + } const rowsNotShownMessage = $('#main-element').data('trans-some-rows-not-shown') const table = $('#resultsTable').DataTable({ @@ -188,20 +188,20 @@ export function pageTestplansSearchReadyHandler () { zeroRecords: 'No records found' }, order: [[2, 'asc']], - initComplete: function(){ - const btnContainer = table.buttons().container(); + initComplete: function () { + const btnContainer = table.buttons().container() $(btnContainer).prepend( '' - ); - + ) + // Hook the checkbox change event to “select all” - $('#onlyActive').on('change', function(){ + $('#onlyActive').on('change', function () { const checked = this.checked $('#resultsTable tbody input.row-select') .prop('checked', checked) .trigger('change') - }); - } + }) + } }) // row checkbox handler @@ -236,27 +236,32 @@ export function pageTestplansSearchReadyHandler () { } function getSelectedTestPlans () { - const inputs = $('#resultsTable tbody input.row-select:checked').closest('tr') + const inputs = $('#resultsTable tbody input.row-select:checked').closest('tr:visible') const tpIds = [] inputs.each(function (_, el) { - const id = $(el).closest('tr').find('td:nth-child(3)').text().trim() - if (id) { - tpIds.push(id) - } - // Check if the row has collapsed children and add their IDs - const row = $('#resultsTable').DataTable().row($(el).closest('tr')) - if (hiddenChildRows[id] && !row.child.isShown()) { - hiddenChildRows[id].forEach(function (childRow) { - const childId = $(childRow).find('td:nth-child(3)').text().trim() - if (childId) { - tpIds.push(childId) - } - }) - } + tpIds.push(...getChildRows(el)) }) + return tpIds +} + +function getChildRows (parentRowId) { + const tpIds = [] + const parentRow = $('#resultsTable').DataTable().row($(parentRowId).closest('tr')) + const id = $(parentRowId).closest('tr').find('td:nth-child(3)').text().trim() + const children = hiddenChildRows[id] + if (id) { + tpIds.push(id) + } + + if (children && !parentRow.child.isShown()) { + children.forEach(function (childRow) { + tpIds.push(...getChildRows(childRow)) + }) + return tpIds + } return tpIds } From b98c2112e82bcc189fd2122815610c8436efcae7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 00:00:44 +0000 Subject: [PATCH 8/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcms/testplans/static/testplans/js/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcms/testplans/static/testplans/js/search.js b/tcms/testplans/static/testplans/js/search.js index a7c3cd1b46..8a9ae37778 100644 --- a/tcms/testplans/static/testplans/js/search.js +++ b/tcms/testplans/static/testplans/js/search.js @@ -193,7 +193,7 @@ export function pageTestplansSearchReadyHandler () { $(btnContainer).prepend( '' ) - + // Hook the checkbox change event to “select all” $('#onlyActive').on('change', function () { const checked = this.checked
{% trans "ID" %} {% trans "Test plan" %}