diff --git a/tcms/testplans/forms.py b/tcms/testplans/forms.py index d3e817bc54..df69a3a74e 100644 --- a/tcms/testplans/forms.py +++ b/tcms/testplans/forms.py @@ -82,3 +82,33 @@ 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..8a9ae37778 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'), @@ -93,6 +107,11 @@ export function pageTestplansSearchReadyHandler () { dataTableJsonRPC('TestPlan.filter', params, callbackF, preProcessData) }, columns: [ + { + data: null, + orderable: false, + render: function () { return '' } + }, { data: null, defaultContent: '', @@ -159,13 +178,37 @@ export function pageTestplansSearchReadyHandler () { }) }, dom: 'Bptp', - buttons: exportButtons, + buttons: [ + multiCloneButton, + ...exportButtons + ], language: { loadingRecords: '
', processing: '', zeroRecords: 'No records found' }, - order: [[1, 'asc']] + 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 + $('#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 @@ -192,6 +235,36 @@ export function pageTestplansSearchReadyHandler () { $('#id_product').change(updateVersionSelectFromProduct) } +function getSelectedTestPlans () { + const inputs = $('#resultsTable tbody input.row-select:checked').closest('tr:visible') + const tpIds = [] + + inputs.each(function (_, el) { + // Check if the row has collapsed children and add their IDs + 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 +} + function hideExpandedChildren (table, parentRow) { const children = hiddenChildRows[parentRow.data().id] children.forEach( @@ -214,5 +287,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 %} +