1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

Refactor admin inline actions and fix tabular inline handling.

Simplified and reorganized JavaScript for managing inline actions, improving readability and reusability. Updated CSS class selectors and templates to ensure consistent behavior across inline tabular forms. Adjusted test cases to reflect the changes and maintain compatibility.
This commit is contained in:
tanaydin 2024-12-12 22:54:40 +01:00
parent 6f38697f90
commit d48b0046c1
3 changed files with 307 additions and 50 deletions

View File

@ -467,6 +467,10 @@ body.popup .submit-row {
font-size: 0.75rem;
}
.inline-group tr:has(td.delete input:checked) {
background-color: var(--selected-row);
}
.empty-form {
display: none;
}

View File

@ -85,7 +85,8 @@
acrossQuestions: "div.actions span.question",
acrossClears: "div.actions span.clear",
allToggleId: "action-toggle",
selectedClass: "selected"
selectedClass: "selected",
inline: false,
};
window.Actions = function(actionCheckboxes, options) {
@ -102,32 +103,6 @@
shiftPressed = event.shiftKey;
});
document.getElementById(options.allToggleId).addEventListener('click', function(event) {
checker(actionCheckboxes, options, this.checked);
updateCounter(actionCheckboxes, options);
});
document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) {
el.addEventListener('click', function(event) {
event.preventDefault();
const acrossInputs = document.querySelectorAll(options.acrossInput);
acrossInputs.forEach(function(acrossInput) {
acrossInput.value = 1;
});
showClear(options);
});
});
document.querySelectorAll(options.acrossClears + " a").forEach(function(el) {
el.addEventListener('click', function(event) {
event.preventDefault();
document.getElementById(options.allToggleId).checked = false;
clearAcross(options);
checker(actionCheckboxes, options, false);
updateCounter(actionCheckboxes, options);
});
});
function affectedCheckboxes(target, withModifier) {
const multiSelect = (lastChecked && withModifier && lastChecked !== target);
if (!multiSelect) {
@ -138,32 +113,91 @@
const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked);
const startIndex = Math.min(targetIndex, lastCheckedIndex);
const endIndex = Math.max(targetIndex, lastCheckedIndex);
const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex));
return filtered;
};
return checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex));
}
Array.from(document.getElementById('result_list').tBodies).forEach(function(el) {
el.addEventListener('change', function(event) {
const target = event.target;
if (target.classList.contains('action-select')) {
const checkboxes = affectedCheckboxes(target, shiftPressed);
checker(checkboxes, options, target.checked);
if (!options.inline) {
document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) {
el.addEventListener('click', function(event) {
event.preventDefault();
const acrossInputs = document.querySelectorAll(options.acrossInput);
acrossInputs.forEach(function(acrossInput) {
acrossInput.value = 1;
});
showClear(options);
});
});
document.querySelectorAll(options.acrossClears + " a").forEach(function(el) {
el.addEventListener('click', function(event) {
event.preventDefault();
document.getElementById(options.allToggleId).checked = false;
clearAcross(options);
checker(actionCheckboxes, options, false);
updateCounter(actionCheckboxes, options);
lastChecked = target;
} else {
list_editable_changed = true;
});
});
document.getElementById(options.allToggleId).addEventListener('click', function(event) {
checker(actionCheckboxes, options, this.checked);
updateCounter(actionCheckboxes, options);
});
Array.from(document.getElementById('result_list').tBodies).forEach(
function(el) {
el.addEventListener('change', function(event) {
const target = event.target;
if (target.classList.contains('action-select')) {
const checkboxes = affectedCheckboxes(target, shiftPressed);
checker(checkboxes, options, target.checked);
updateCounter(actionCheckboxes, options);
lastChecked = target;
} else {
list_editable_changed = true;
}
});
});
document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) {
if (list_editable_changed) {
const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
if (!confirmed) {
event.preventDefault();
}
}
});
});
// Sync counter when navigating to the page, such as through the back
// button.
window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options));
} else if (options.inline) {
const handleCheckboxChange = (event) => {
const target = event.target;
document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) {
if (list_editable_changed) {
const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
if (!confirmed) {
event.preventDefault();
if (!lastChecked || !target.name.endsWith('-DELETE')) {
lastChecked = target;
return;
}
}
});
if (lastChecked.name.slice(0, -9) === target.name.slice(0, -9)) {
// Checking for if clicked checkboxes are in the same form with forms common prefix.
const checkboxes = affectedCheckboxes(target, shiftPressed);
checker(checkboxes, options, target.checked);
}
lastChecked = target;
};
const attachChangeListener = (element) => {
if (!element) {
return;
}
element.addEventListener('change', handleCheckboxChange);
};
// Handle tabular inline tables
document.querySelectorAll('.tabular tbody').forEach(attachChangeListener);
// Handle stacked inlines
document.querySelectorAll('.inline-related').forEach(attachChangeListener);
}
const el = document.querySelector('#changelist-form input[name=_save]');
// The button does not exist if no fields are editable.
@ -179,9 +213,6 @@
}
});
}
// Sync counter when navigating to the page, such as through the back
// button.
window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options));
};
// Call function fn when the DOM is loaded and ready. If it is already
@ -200,5 +231,16 @@
if (actionsEls.length > 0) {
Actions(actionsEls);
}
const tabularActionsEls = document.querySelectorAll(
'td.delete input[type="checkbox"]');
if (tabularActionsEls.length > 0) {
defaults.inline = true;
Actions(tabularActionsEls);
}
const stackedActionsEls = document.querySelectorAll('span.delete input[type="checkbox"]');
if (stackedActionsEls.length > 0) {
defaults.inline = true;
Actions(stackedActionsEls);
}
});
}

View File

@ -1931,6 +1931,217 @@ class SeleniumTests(AdminSeleniumTestCase):
with self.disable_implicit_wait():
self.assertCountSeleniumElements(rows_selector, 0)
def test_delete_multiselect_tabular_inline(self):
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
def fill_form_data():
self.selenium.find_element(By.ID, "id_name").send_keys("Family Name")
# Fill inline forms
for group in [1, 2]:
for i in range(3):
self.selenium.find_element(
By.ID, f"id_{-group}-{i}-name"
).send_keys(f"person name {group} {i+1}")
def get_delete_checkboxes():
checkboxes = {}
for group in range(1, 5):
for i in range(3):
key = f"{group}_{i}"
checkboxes[key] = self.selenium.find_element(
By.ID, f"id_-{group}-{i}-DELETE"
)
return checkboxes
def scroll_to_element(element):
self.selenium.execute_script(
"window.scrollTo(0, %s);" % element.location["y"]
)
def shift_click_range(start_element, end_element):
actions = ActionChains(self.selenium)
actions.move_to_element(start_element)
actions.key_down(Keys.SHIFT)
actions.click()
actions.move_to_element(end_element)
actions.click()
actions.key_up(Keys.SHIFT)
actions.perform()
def assert_checkbox_states(checkboxes, expected_states):
for key, expected in expected_states.items():
actual = checkboxes[key].get_property("checked")
self.assertEqual(
actual,
expected,
f"Checkbox {key} state mismatch."
f" Expected: {expected}, Got: {actual}",
)
# Setup
self.admin_login(username="super", password="secret")
self.selenium.get(
self.live_server_url + reverse("admin:admin_inlines_capofamiglia_add")
)
# Fill and save form
fill_form_data()
with self.wait_page_loaded():
btn = self.selenium.find_element(
By.XPATH, '//input[@value="Save and continue editing"]'
)
scroll_to_element(btn)
btn.click()
# Get all delete checkboxes
checkboxes = get_delete_checkboxes()
# Test 1: Initial state - all unchecked
assert_checkbox_states(
checkboxes,
{
"1_0": False,
"1_1": False,
"1_2": False,
"2_0": False,
"2_1": False,
"2_2": False,
"3_0": False,
"3_1": False,
"3_2": False,
"4_0": False,
"4_1": False,
"4_2": False,
},
)
# Test 2: Select first row completely
scroll_to_element(checkboxes["1_0"])
shift_click_range(checkboxes["1_0"], checkboxes["1_2"])
assert_checkbox_states(
checkboxes,
{
"1_0": True,
"1_1": True,
"1_2": True,
"2_0": False,
"2_1": False,
"2_2": False,
},
)
# Test 3: Unselect middle checkbox in first row
scroll_to_element(checkboxes["1_0"])
shift_click_range(checkboxes["1_2"], checkboxes["1_0"])
assert_checkbox_states(
checkboxes,
{
"1_0": False,
"1_1": False,
"1_2": False,
"2_0": False,
"2_1": False,
"2_2": False,
},
)
# Test 4: Select across rows
actions = ActionChains(self.selenium)
scroll_to_element(checkboxes["1_0"])
actions.move_to_element(checkboxes["1_0"]).key_down(
Keys.SHIFT
).click().perform()
scroll_to_element(checkboxes["2_2"])
actions.move_to_element(checkboxes["2_2"]).click().perform()
assert_checkbox_states(
checkboxes,
{
"1_0": True,
"1_1": False,
"1_2": False,
"2_0": False,
"2_1": False,
"2_2": True,
},
)
# Test 5: Select all remaining checkboxes
scroll_to_element(checkboxes["1_0"])
shift_click_range(checkboxes["1_0"], checkboxes["1_2"])
scroll_to_element(checkboxes["2_0"])
shift_click_range(checkboxes["2_0"], checkboxes["2_1"])
assert_checkbox_states(
checkboxes,
{
"1_0": True,
"1_1": True,
"1_2": True,
"2_0": True,
"2_1": True,
"2_2": True,
},
)
scroll_to_element(checkboxes["3_0"])
shift_click_range(checkboxes["3_0"], checkboxes["3_2"])
assert_checkbox_states(
checkboxes,
{
"3_0": True,
"3_1": True,
"3_2": True,
"4_0": False,
"4_1": False,
"4_2": False,
},
)
scroll_to_element(checkboxes["3_0"])
shift_click_range(checkboxes["3_2"], checkboxes["3_0"])
assert_checkbox_states(
checkboxes,
{
"3_0": False,
"3_1": False,
"3_2": False,
"4_0": False,
"4_1": False,
"4_2": False,
},
)
actions = ActionChains(self.selenium)
scroll_to_element(checkboxes["3_0"])
actions.move_to_element(checkboxes["3_0"]).key_down(
Keys.SHIFT
).click().perform()
scroll_to_element(checkboxes["4_2"])
actions.move_to_element(checkboxes["4_2"]).click().perform()
assert_checkbox_states(
checkboxes,
{
"3_0": True,
"3_1": False,
"3_2": False,
"4_0": False,
"4_1": False,
"4_2": True,
},
)
scroll_to_element(checkboxes["1_0"])
shift_click_range(checkboxes["1_0"], checkboxes["1_2"])
scroll_to_element(checkboxes["2_0"])
shift_click_range(checkboxes["2_0"], checkboxes["2_2"])
scroll_to_element(checkboxes["3_0"])
shift_click_range(checkboxes["3_0"], checkboxes["3_2"])
scroll_to_element(checkboxes["4_0"])
shift_click_range(checkboxes["4_0"], checkboxes["4_2"])
btn2 = self.selenium.find_element(By.XPATH, '//input[@value="Save"]')
scroll_to_element(btn2)
btn2.click()
def test_delete_invalid_stacked_inlines(self):
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By