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:
parent
6f38697f90
commit
d48b0046c1
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user