Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions tcms/static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,186 @@ export function showOrHideMultipleRows (rootSelector, rows) {
}
}

/*
Reusable duplicate-check logic for "new" forms.

config = {
rpcMethod: 'TestCase.filter', // JSON-RPC method
fieldName: 'summary', // model field name
inputSelector: '#id_summary', // input element
groupSelector: '#summary-group', // form-group with data-trans-* attrs
warningSelector:'#duplicate-summary-warning',
autocompleteSelector: '#summary-autocomplete',
modalSelector: '#duplicate-tc-modal',
prefix: 'TC', // display prefix, e.g. TC, TR, TP
detailUrlBase: '/case/', // URL path for detail view
modalRows: function(g, item) {}, // returns array of [label, value]
descriptionField: 'text' // field name for markdown description, or null
}
*/
export function initDuplicateCheck (config) {
const duplicateWarning = $(config.warningSelector)
if (!duplicateWarning.length) {
return
}

const group = $(config.groupSelector)
const input = $(config.inputSelector)
const autocomplete = $(config.autocompleteSelector)
const modal = $(config.modalSelector)
const maxItems = 10
let debounceTimer = null
let duplicateMatches = []

function showModal (item) {
const g = group.data.bind(group)
$('#duplicate-modal-title').text(
g('trans-duplicate-modal-title') + ' - ' + config.prefix + '-' + item.id
)

$('#duplicate-modal-view-btn').attr('href', config.detailUrlBase + item.id + '/')
$('#duplicate-modal-view-label').text(
g('trans-duplicate-modal-view') + ' ' + config.prefix + '-' + item.id
)

const body = $('#duplicate-modal-body')
body.empty()

const table = $('<table>', { class: 'table table-striped table-condensed' })
const rows = config.modalRows(g, item)
rows.forEach(function (row) {
$('<tr>')
.append($('<th>', { text: row[0], css: { width: '150px' } }))
.append($('<td>', { text: row[1] || '' }))
.appendTo(table)
})

if (config.descriptionField) {
const descriptionContainer = $('<div>', {
css: {
'max-height': '300px',
'overflow-y': 'auto',
'background-color': '#f5f5f5',
padding: '10px',
'border-radius': '4px'
}
})
const descriptionRow = $('<tr>')
.append($('<th>', { text: g('trans-duplicate-modal-text'), css: { width: '150px', 'vertical-align': 'top' } }))
.append($('<td>').append(descriptionContainer))
table.append(descriptionRow)
body.append(table)

const descValue = item[config.descriptionField]
if (descValue) {
markdown2HTML(descValue, descriptionContainer[0])
} else {
descriptionContainer.text('-')
}
} else {
body.append(table)
}

modal.modal('show')
}

function updateWarning () {
if (duplicateMatches.length > 0) {
duplicateWarning.html(
'<span class="fa fa-exclamation-triangle"></span> ' +
group.data('trans-duplicate-blocked')
).removeClass('hidden')
} else {
duplicateWarning.addClass('hidden').empty()
}
}

const fieldContains = config.fieldName + '__icontains'
const fieldIexact = config.fieldName + '__iexact'

input.on('input', function () {
const value = $(this).val().trim()
clearTimeout(debounceTimer)

if (value.length < 3) {
duplicateWarning.addClass('hidden').empty()
autocomplete.hide().empty()
duplicateMatches = []
return
}

const filterParam = {}
filterParam[fieldContains] = value

debounceTimer = setTimeout(function () {
jsonRPC(config.rpcMethod, filterParam, function (data) {
duplicateMatches = data.filter(function (item) {
return item[config.fieldName].toLowerCase() === value.toLowerCase()
})
updateWarning()

autocomplete.empty()
if (data.length > 0) {
data.slice(0, maxItems).forEach(function (item) {
$('<a>', {
href: '#',
class: 'list-group-item',
text: config.prefix + '-' + item.id + ': ' + item[config.fieldName]
}).on('click', function (e) {
e.preventDefault()
autocomplete.hide()
showModal(item)
}).appendTo(autocomplete)
})
if (data.length > maxItems) {
$('<span>', {
class: 'list-group-item disabled',
text: '... and ' + (data.length - maxItems) + ' more'
}).appendTo(autocomplete)
}
autocomplete.show()
} else {
autocomplete.hide()
}
})
}, 500)
})

$(document).on('click', function (e) {
if (!$(e.target).closest(config.inputSelector + ', ' + config.autocompleteSelector).length) {
autocomplete.hide()
}
})

input.on('focus', function () {
if (autocomplete.children().length > 0) {
autocomplete.show()
}
})

duplicateWarning.closest('form').on('submit', function (e) {
const currentValue = input.val().trim()
if (currentValue.length < 1) {
return
}

const exactParam = {}
exactParam[fieldIexact] = currentValue

duplicateMatches = []
jsonRPC(config.rpcMethod, exactParam, function (data) {
duplicateMatches = data
}, true)

if (duplicateMatches.length > 0) {
e.preventDefault()
updateWarning()
showModal(duplicateMatches[0])
return false
}
})
}

export function discoverNestedTestPlans (inputData, callbackF) {
const prefix = '&nbsp;&nbsp;&nbsp;&nbsp;'
const result = []
Expand Down
22 changes: 22 additions & 0 deletions tcms/templates/include/duplicate_check_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% load i18n %}
<div class="modal fade" id="duplicate-modal" tabindex="-1" role="dialog" aria-labelledby="duplicate-modal-title" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" aria-label="Close">
<span class="pficon pficon-close"></span>
</button>
<a id="duplicate-modal-view-btn" href="#" target="_blank" class="btn btn-default btn-sm pull-right" style="margin-right: 10px;">
<span class="fa fa-external-link"></span> <span id="duplicate-modal-view-label"></span>
</a>
<h4 class="modal-title" id="duplicate-modal-title"></h4>
</div>
<div class="modal-body">
<div id="duplicate-modal-body"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
</div>
</div>
</div>
</div>
24 changes: 23 additions & 1 deletion tcms/testcases/static/testcases/js/mutable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { updateCategorySelectFromProduct } from '../../../../static/js/utils'
import { initDuplicateCheck, updateCategorySelectFromProduct } from '../../../../static/js/utils'

export function pageTestcasesMutableReadyHandler () {
$('#id_template').change(function () {
Expand Down Expand Up @@ -51,6 +51,28 @@ export function pageTestcasesMutableReadyHandler () {
showMinutes: true,
showSeconds: true
})

initDuplicateCheck({
rpcMethod: 'TestCase.filter',
fieldName: 'summary',
inputSelector: '#id_summary',
groupSelector: '#summary-group',
warningSelector: '#duplicate-summary-warning',
autocompleteSelector: '#summary-autocomplete',
modalSelector: '#duplicate-modal',
prefix: 'TC',
detailUrlBase: '/case/',
descriptionField: 'text',
modalRows: function (g, tc) {
return [
[g('trans-duplicate-modal-summary'), tc.summary],
[g('trans-duplicate-modal-status'), tc.case_status__name],
[g('trans-duplicate-modal-category'), tc.category__name],
[g('trans-duplicate-modal-priority'), tc.priority__value],
[g('trans-duplicate-modal-author'), tc.author__username]
]
}
})
}

function populateProductCategory () {
Expand Down
24 changes: 22 additions & 2 deletions tcms/testcases/templates/testcases/mutable.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,38 @@
<form class="form-horizontal" action="{% if object %}{% url 'testcases-edit' object.pk %}{% else %}{% url 'testcases-new' %}{% endif %}" method="post">
{% csrf_token %}
<input type="hidden" name="author" value="{{ form.author.value }}">
<div class="form-group">
<div class="form-group" id="summary-group"
data-trans-duplicate-blocked="{% trans 'A test case with this exact summary already exists. Duplicates are not allowed.' %}"
data-trans-duplicate-modal-title="{% trans 'Duplicate test case found' %}"
data-trans-duplicate-modal-summary="{% trans 'Summary' %}"
data-trans-duplicate-modal-author="{% trans 'Author' %}"
data-trans-duplicate-modal-status="{% trans 'Status' %}"
data-trans-duplicate-modal-category="{% trans 'Category' %}"
data-trans-duplicate-modal-priority="{% trans 'Priority' %}"
data-trans-duplicate-modal-text="{% trans 'Description' %}"
data-trans-duplicate-modal-view="{% trans 'View existing test case' %}">
<label class="col-md-1 col-lg-1" for="id_summary">{% trans "Summary" %}</label>
<div class="col-md-11 col-lg-11 {% if form.summary.errors %}has-error{% endif %}">
<input type="text" id="id_summary" name="summary" value="{{ form.summary.value|default:'' }}" class="form-control" required>
<input type="text" id="id_summary" name="summary" value="{{ form.summary.value|default:'' }}" class="form-control" required autocomplete="off">
{% if not object %}
<div id="summary-autocomplete" class="list-group" style="display: none; max-height: 200px; overflow-y: auto; margin-bottom: 0; background-color: #f5f5f5; border: 1px solid #b7b7b7;"></div>
{% endif %}
{% if test_plan %}
<p class="help-block"><a href="{% url 'test_plan_url' test_plan.pk %}">TP-{{ test_plan.pk }}: {{ test_plan.name }}</a></p>
<input type="hidden" name="from_plan" value="{{ test_plan.pk }}">
{% endif %}
{{ form.summary.errors }}
{% if not object %}
<div id="duplicate-summary-warning" class="help-block hidden kiwi-color-warning">
</div>
{% endif %}
</div>
</div>

{% if not object %}
{% include "include/duplicate_check_modal.html" %}
{% endif %}

<div class="form-group">
<label class="col-md-1 col-lg-1" for="id_default_tester">{% trans "Default tester" %}</label>
<div class="col-md-3 col-lg-3 {% if form.default_tester.errors %}has-error{% endif %}">
Expand Down
24 changes: 23 additions & 1 deletion tcms/testplans/static/testplans/js/mutable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { testPlanAutoComplete } from '../../../../static/js/jsonrpc'
import { populateVersion } from '../../../../static/js/utils'
import { initDuplicateCheck, populateVersion } from '../../../../static/js/utils'

const planCache = {}

Expand Down Expand Up @@ -50,4 +50,26 @@ export function pageTestplansMutableReadyHandler () {

// override the default inline-block style
$('span.twitter-typeahead').css('display', 'block')

initDuplicateCheck({
rpcMethod: 'TestPlan.filter',
fieldName: 'name',
inputSelector: '#id_name',
groupSelector: '#name-group',
warningSelector: '#duplicate-name-warning',
autocompleteSelector: '#name-autocomplete',
modalSelector: '#duplicate-modal',
prefix: 'TP',
detailUrlBase: '/plan/',
descriptionField: 'text',
modalRows: function (g, tp) {
return [
[g('trans-duplicate-modal-name'), tp.name],
[g('trans-duplicate-modal-product'), tp.product__name],
[g('trans-duplicate-modal-version'), tp.product_version__value],
[g('trans-duplicate-modal-type'), tp.type__name],
[g('trans-duplicate-modal-author'), tp.author__username]
]
}
})
}
26 changes: 23 additions & 3 deletions tcms/testplans/templates/testplans/mutable.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,34 @@
<form class="form-horizontal" action="{% if object %}{% url 'plan-edit' object.pk %}{% else %}{% url 'plans-new' %}{% endif %}" method="post">
{% csrf_token %}
<input type="hidden" name="author" value="{{ form.author.value }}">
<div class="form-group">
<div class="form-group" id="name-group"
data-trans-duplicate-blocked="{% trans 'A test plan with this exact name already exists. Duplicates are not allowed.' %}"
data-trans-duplicate-modal-title="{% trans 'Duplicate test plan found' %}"
data-trans-duplicate-modal-name="{% trans 'Name' %}"
data-trans-duplicate-modal-product="{% trans 'Product' %}"
data-trans-duplicate-modal-version="{% trans 'Version' %}"
data-trans-duplicate-modal-type="{% trans 'Type' %}"
data-trans-duplicate-modal-author="{% trans 'Author' %}"
data-trans-duplicate-modal-text="{% trans 'Description' %}"
data-trans-duplicate-modal-view="{% trans 'View existing test plan' %}">
<label class="col-md-1 col-lg-1" for="id_name">{% trans "Name" %}</label>
<div class="col-md-11 col-lg-11 {% if form.name.errors %}has-error{% endif %}">
<input type="text" id="id_name" name="name" value="{{ form.name.value|default:'' }}" class="form-control" required>
{{ form.name.errors }}
<input type="text" id="id_name" name="name" value="{{ form.name.value|default:'' }}" class="form-control" required autocomplete="off">
{% if not object %}
<div id="name-autocomplete" class="list-group" style="display: none; max-height: 200px; overflow-y: auto; margin-bottom: 0; background-color: #f5f5f5; border: 1px solid #b7b7b7;"></div>
{% endif %}
{{ form.name.errors }}
{% if not object %}
<div id="duplicate-name-warning" class="help-block hidden kiwi-color-warning">
</div>
{% endif %}
</div>
</div>

{% if not object %}
{% include "include/duplicate_check_modal.html" %}
{% endif %}

<div class="form-group">
<div class="col-md-1 col-lg-1">
<label for="id_product">{% trans "Product" %}</label>
Expand Down
23 changes: 22 additions & 1 deletion tcms/testruns/static/testruns/js/mutable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { initializeDateTimePicker } from '../../../../static/js/datetime_picker'
import { jsonRPC } from '../../../../static/js/jsonrpc'
import { discoverNestedTestPlans, updateSelect, updateTestPlanSelectFromProduct } from '../../../../static/js/utils'
import { discoverNestedTestPlans, initDuplicateCheck, updateSelect, updateTestPlanSelectFromProduct } from '../../../../static/js/utils'

export function pageTestrunsMutableReadyHandler () {
initializeDateTimePicker('#id_planned_start')
Expand Down Expand Up @@ -42,4 +42,25 @@ export function pageTestrunsMutableReadyHandler () {
$('#add_id_build').click(function () {
return showRelatedObjectPopup(this)
})

initDuplicateCheck({
rpcMethod: 'TestRun.filter',
fieldName: 'summary',
inputSelector: '#id_summary',
groupSelector: '#summary-group',
warningSelector: '#duplicate-summary-warning',
autocompleteSelector: '#summary-autocomplete',
modalSelector: '#duplicate-modal',
prefix: 'TR',
detailUrlBase: '/runs/',
descriptionField: 'notes',
modalRows: function (g, tr) {
return [
[g('trans-duplicate-modal-summary'), tr.summary],
[g('trans-duplicate-modal-plan'), tr.plan__name],
[g('trans-duplicate-modal-build'), tr.build__name],
[g('trans-duplicate-modal-manager'), tr.manager__username]
]
}
})
}
Loading