mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-09-19 15:39:32 +02:00
Use modal dialog for confirming actions (#1168)
* Use modal dialog for confirming actions * cleanup unused state
This commit is contained in:
@@ -1,79 +1,173 @@
|
|||||||
import { Behavior, registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
import { FocusTrapController, isKeyboardActive } from "./focus-utils";
|
||||||
|
|
||||||
|
let confirmId = 0;
|
||||||
|
|
||||||
|
function nextConfirmId() {
|
||||||
|
return `confirm-${confirmId++}`;
|
||||||
|
}
|
||||||
|
|
||||||
class ConfirmButtonBehavior extends Behavior {
|
class ConfirmButtonBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
super(element);
|
super(element);
|
||||||
|
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
element.addEventListener("click", this.onClick);
|
this.element.addEventListener("click", this.onClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.reset();
|
if (this.opened) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
this.element.removeEventListener("click", this.onClick);
|
this.element.removeEventListener("click", this.onClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event) {
|
onClick(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
Behavior.interacting = true;
|
|
||||||
|
|
||||||
const container = document.createElement("span");
|
if (this.opened) {
|
||||||
container.className = "confirmation";
|
this.close();
|
||||||
|
} else {
|
||||||
const icon = this.element.getAttribute("ld-confirm-icon");
|
this.open();
|
||||||
if (icon) {
|
|
||||||
const iconElement = document.createElementNS(
|
|
||||||
"http://www.w3.org/2000/svg",
|
|
||||||
"svg",
|
|
||||||
);
|
|
||||||
iconElement.style.width = "16px";
|
|
||||||
iconElement.style.height = "16px";
|
|
||||||
iconElement.innerHTML = `<use xlink:href="#${icon}"></use>`;
|
|
||||||
container.append(iconElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const question = this.element.getAttribute("ld-confirm-question");
|
|
||||||
if (question) {
|
|
||||||
const questionElement = document.createElement("span");
|
|
||||||
questionElement.innerText = question;
|
|
||||||
container.append(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonClasses = Array.from(this.element.classList.values())
|
|
||||||
.filter((cls) => cls.startsWith("btn"))
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const cancelButton = document.createElement(this.element.nodeName);
|
|
||||||
cancelButton.type = "button";
|
|
||||||
cancelButton.innerText = question ? "No" : "Cancel";
|
|
||||||
cancelButton.className = `${buttonClasses} mr-1`;
|
|
||||||
cancelButton.addEventListener("click", this.reset.bind(this));
|
|
||||||
|
|
||||||
const confirmButton = document.createElement(this.element.nodeName);
|
|
||||||
confirmButton.type = this.element.type;
|
|
||||||
confirmButton.name = this.element.name;
|
|
||||||
confirmButton.value = this.element.value;
|
|
||||||
confirmButton.innerText = question ? "Yes" : "Confirm";
|
|
||||||
confirmButton.className = buttonClasses;
|
|
||||||
confirmButton.addEventListener("click", this.reset.bind(this));
|
|
||||||
|
|
||||||
container.append(cancelButton, confirmButton);
|
|
||||||
this.container = container;
|
|
||||||
|
|
||||||
this.element.before(container);
|
|
||||||
this.element.classList.add("d-none");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
open() {
|
||||||
setTimeout(() => {
|
const dropdown = document.createElement("div");
|
||||||
Behavior.interacting = false;
|
dropdown.className = "dropdown confirm-dropdown active";
|
||||||
if (this.container) {
|
|
||||||
this.container.remove();
|
const confirmId = nextConfirmId();
|
||||||
this.container = null;
|
const questionId = `${confirmId}-question`;
|
||||||
}
|
|
||||||
this.element.classList.remove("d-none");
|
const menu = document.createElement("div");
|
||||||
|
menu.className = "menu with-arrow";
|
||||||
|
menu.role = "alertdialog";
|
||||||
|
menu.setAttribute("aria-modal", "true");
|
||||||
|
menu.setAttribute("aria-labelledby", questionId);
|
||||||
|
menu.addEventListener("keydown", this.onMenuKeyDown.bind(this));
|
||||||
|
|
||||||
|
const question = document.createElement("span");
|
||||||
|
question.id = questionId;
|
||||||
|
question.textContent =
|
||||||
|
this.element.getAttribute("ld-confirm-question") || "Are you sure?";
|
||||||
|
question.style.fontWeight = "bold";
|
||||||
|
|
||||||
|
const cancelButton = document.createElement("button");
|
||||||
|
cancelButton.textContent = "Cancel";
|
||||||
|
cancelButton.type = "button";
|
||||||
|
cancelButton.className = "btn";
|
||||||
|
cancelButton.tabIndex = 0;
|
||||||
|
cancelButton.addEventListener("click", () => this.close());
|
||||||
|
|
||||||
|
const confirmButton = document.createElement("button");
|
||||||
|
confirmButton.textContent = "Confirm";
|
||||||
|
confirmButton.type = "submit";
|
||||||
|
confirmButton.name = this.element.name;
|
||||||
|
confirmButton.value = this.element.value;
|
||||||
|
confirmButton.className = "btn btn-error";
|
||||||
|
confirmButton.addEventListener("click", () => this.confirm());
|
||||||
|
|
||||||
|
const arrow = document.createElement("div");
|
||||||
|
arrow.className = "menu-arrow";
|
||||||
|
|
||||||
|
menu.append(question, cancelButton, confirmButton, arrow);
|
||||||
|
dropdown.append(menu);
|
||||||
|
document.body.append(dropdown);
|
||||||
|
|
||||||
|
this.positionController = new AnchorPositionController(this.element, menu);
|
||||||
|
this.focusTrap = new FocusTrapController(menu);
|
||||||
|
this.dropdown = dropdown;
|
||||||
|
this.opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuKeyDown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.element.closest("form").requestSubmit(this.element);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.opened) return;
|
||||||
|
this.positionController.destroy();
|
||||||
|
this.focusTrap.destroy();
|
||||||
|
this.dropdown.remove();
|
||||||
|
this.element.focus({ focusVisible: isKeyboardActive() });
|
||||||
|
this.opened = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnchorPositionController {
|
||||||
|
constructor(anchor, overlay) {
|
||||||
|
this.anchor = anchor;
|
||||||
|
this.overlay = overlay;
|
||||||
|
|
||||||
|
this.handleScroll = this.handleScroll.bind(this);
|
||||||
|
window.addEventListener("scroll", this.handleScroll, { capture: true });
|
||||||
|
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll() {
|
||||||
|
if (this.debounce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debounce = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.updatePosition();
|
||||||
|
this.debounce = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePosition() {
|
||||||
|
const anchorRect = this.anchor.getBoundingClientRect();
|
||||||
|
const overlayRect = this.overlay.getBoundingClientRect();
|
||||||
|
const bufferX = 10;
|
||||||
|
const bufferY = 30;
|
||||||
|
|
||||||
|
let left = anchorRect.left - (overlayRect.width - anchorRect.width) / 2;
|
||||||
|
const initialLeft = left;
|
||||||
|
const overflowLeft = left < bufferX;
|
||||||
|
const overflowRight =
|
||||||
|
left + overlayRect.width > window.innerWidth - bufferX;
|
||||||
|
|
||||||
|
if (overflowLeft) {
|
||||||
|
left = bufferX;
|
||||||
|
} else if (overflowRight) {
|
||||||
|
left = window.innerWidth - overlayRect.width - bufferX;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = initialLeft - left;
|
||||||
|
this.overlay.style.setProperty("--arrow-offset", `${delta}px`);
|
||||||
|
|
||||||
|
let top = anchorRect.bottom;
|
||||||
|
const overflowBottom =
|
||||||
|
top + overlayRect.height > window.innerHeight - bufferY;
|
||||||
|
|
||||||
|
if (overflowBottom) {
|
||||||
|
top = anchorRect.top - overlayRect.height;
|
||||||
|
this.overlay.classList.remove("top-aligned");
|
||||||
|
this.overlay.classList.add("bottom-aligned");
|
||||||
|
} else {
|
||||||
|
this.overlay.classList.remove("bottom-aligned");
|
||||||
|
this.overlay.classList.add("top-aligned");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.overlay.style.left = `${left}px`;
|
||||||
|
this.overlay.style.top = `${top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
window.removeEventListener("scroll", this.handleScroll, { capture: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerBehavior("ld-confirm-button", ConfirmButtonBehavior);
|
registerBehavior("ld-confirm-button", ConfirmButtonBehavior);
|
||||||
|
@@ -93,6 +93,12 @@ document.addEventListener("turbo:load", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore if there is a modal dialog, which should handle its own focus
|
||||||
|
const modal = document.querySelector("[aria-modal='true']");
|
||||||
|
if (modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there is an explicit focus target for the next page load
|
// Check if there is an explicit focus target for the next page load
|
||||||
for (const target of afterPageLoadFocusTarget) {
|
for (const target of afterPageLoadFocusTarget) {
|
||||||
const element = document.querySelector(target);
|
const element = document.querySelector(target);
|
||||||
|
@@ -54,8 +54,6 @@ export class Behavior {
|
|||||||
destroy() {}
|
destroy() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior.interacting = false;
|
|
||||||
|
|
||||||
export function registerBehavior(name, behavior) {
|
export function registerBehavior(name, behavior) {
|
||||||
behaviorRegistry[name] = behavior;
|
behaviorRegistry[name] = behavior;
|
||||||
}
|
}
|
||||||
|
@@ -23,32 +23,22 @@ export class ModalBehavior extends Behavior {
|
|||||||
this.closeButton.removeEventListener("click", this.onClose);
|
this.closeButton.removeEventListener("click", this.onClose);
|
||||||
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keydown", this.onKeyDown);
|
||||||
|
|
||||||
this.clearInert();
|
this.removeScrollLock();
|
||||||
this.focusTrap.destroy();
|
this.focusTrap.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.setupInert();
|
this.setupScrollLock();
|
||||||
this.focusTrap = new FocusTrapController(
|
this.focusTrap = new FocusTrapController(
|
||||||
this.element.querySelector(".modal-container"),
|
this.element.querySelector(".modal-container"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInert() {
|
setupScrollLock() {
|
||||||
// Inert all other elements on the page
|
|
||||||
document
|
|
||||||
.querySelectorAll("body > *:not(.modals)")
|
|
||||||
.forEach((el) => el.setAttribute("inert", ""));
|
|
||||||
// Lock scroll on the body
|
|
||||||
document.body.classList.add("scroll-lock");
|
document.body.classList.add("scroll-lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
clearInert() {
|
removeScrollLock() {
|
||||||
// Clear inert attribute from all elements to allow focus outside the modal again
|
|
||||||
document
|
|
||||||
.querySelectorAll("body > *")
|
|
||||||
.forEach((el) => el.removeAttribute("inert"));
|
|
||||||
// Remove scroll lock from the body
|
|
||||||
document.body.classList.remove("scroll-lock");
|
document.body.classList.remove("scroll-lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +75,7 @@ export class ModalBehavior extends Behavior {
|
|||||||
|
|
||||||
doClose() {
|
doClose() {
|
||||||
this.element.remove();
|
this.element.remove();
|
||||||
this.clearInert();
|
this.removeScrollLock();
|
||||||
this.element.dispatchEvent(new CustomEvent("modal:close"));
|
this.element.dispatchEvent(new CustomEvent("modal:close"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,22 +31,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Confirm button component */
|
/* Confirm button component */
|
||||||
span.confirmation {
|
.confirm-dropdown.active {
|
||||||
display: flex;
|
position: fixed;
|
||||||
align-items: baseline;
|
z-index: 500;
|
||||||
gap: var(--unit-1);
|
|
||||||
color: var(--error-color) !important;
|
|
||||||
|
|
||||||
svg {
|
& .menu {
|
||||||
align-self: center;
|
position: fixed;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.btn.btn-link {
|
box-sizing: border-box;
|
||||||
color: var(--error-color) !important;
|
gap: var(--unit-2);
|
||||||
|
padding: var(--unit-2);
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -87,4 +87,43 @@
|
|||||||
border-bottom: solid 1px var(--secondary-border-color);
|
border-bottom: solid 1px var(--secondary-border-color);
|
||||||
margin: var(--unit-2) 0;
|
margin: var(--unit-2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-arrow {
|
||||||
|
overflow: visible;
|
||||||
|
--arrow-size: 16px;
|
||||||
|
--arrow-offset: 0px;
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: calc(50% + var(--arrow-offset));
|
||||||
|
top: 0;
|
||||||
|
width: var(--arrow-size);
|
||||||
|
height: var(--arrow-size);
|
||||||
|
translate: -50% -50%;
|
||||||
|
rotate: 45deg;
|
||||||
|
background: inherit;
|
||||||
|
border: inherit;
|
||||||
|
clip-path: polygon(0 0, 0 100%, 100% 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top-aligned {
|
||||||
|
transform: translateY(
|
||||||
|
calc(calc(var(--arrow-size) / 2) + var(--layout-spacing-sm))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom-aligned {
|
||||||
|
transform: translateY(
|
||||||
|
calc(calc(calc(var(--arrow-size) / 2) + var(--layout-spacing-sm)) * -1)
|
||||||
|
);
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
rotate: 225deg;
|
||||||
|
translate: -50% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -106,7 +106,6 @@
|
|||||||
& .modal-footer {
|
& .modal-footer {
|
||||||
padding: var(--unit-6);
|
padding: var(--unit-6);
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -120,7 +120,7 @@
|
|||||||
{% if bookmark_item.show_mark_as_read %}
|
{% if bookmark_item.show_mark_as_read %}
|
||||||
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
|
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
|
ld-confirm-button ld-confirm-question="Mark as read?">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
<use xlink:href="#ld-icon-unread"></use>
|
<use xlink:href="#ld-icon-unread"></use>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
{% if bookmark_item.show_unshare %}
|
{% if bookmark_item.show_unshare %}
|
||||||
<button type="submit" name="unshare" value="{{ bookmark_item.id }}"
|
<button type="submit" name="unshare" value="{{ bookmark_item.id }}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
|
ld-confirm-button ld-confirm-question="Unshare?">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
<use xlink:href="#ld-icon-share"></use>
|
<use xlink:href="#ld-icon-share"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
<input type="hidden" name="disable_turbo" value="true">
|
<input type="hidden" name="disable_turbo" value="true">
|
||||||
<button ld-confirm-button class="btn btn-error btn-wide"
|
<button ld-confirm-button class="btn btn-error btn-wide"
|
||||||
type="submit" name="remove" value="{{ details.bookmark.id }}">
|
type="submit" name="remove" value="{{ details.bookmark.id }}">
|
||||||
Delete...
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -18,18 +18,6 @@
|
|||||||
<path d="M21 6l0 13"></path>
|
<path d="M21 6l0 13"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="ld-icon-read" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
||||||
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 5.899 -1.096"></path>
|
|
||||||
<path d="M3 6a9 9 0 0 1 2.114 -.884m3.8 -.21c1.07 .17 2.116 .534 3.086 1.094a9 9 0 0 1 9 0"></path>
|
|
||||||
<path d="M3 6v13"></path>
|
|
||||||
<path d="M12 6v2m0 4v7"></path>
|
|
||||||
<path d="M21 6v11"></path>
|
|
||||||
<path d="M3 3l18 18"></path>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="ld-icon-share" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
<symbol id="ld-icon-share" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -41,18 +29,6 @@
|
|||||||
<path d="M8.7 13.3l6.6 3.4"></path>
|
<path d="M8.7 13.3l6.6 3.4"></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="ld-icon-unshare" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
|
||||||
<path d="M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
|
||||||
<path d="M18 6m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
|
||||||
<path d="M15.861 15.896a3 3 0 0 0 4.265 4.22m.578 -3.417a3.012 3.012 0 0 0 -1.507 -1.45"></path>
|
|
||||||
<path d="M8.7 10.7l1.336 -.688m2.624 -1.352l2.64 -1.36"></path>
|
|
||||||
<path d="M8.7 13.3l6.6 3.4"></path>
|
|
||||||
<path d="M3 3l18 18"></path>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="ld-icon-note" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
<symbol id="ld-icon-note" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
@@ -501,7 +501,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||||||
modal = self.get_index_details_modal(bookmark)
|
modal = self.get_index_details_modal(bookmark)
|
||||||
delete_button = modal.find("button", {"type": "submit", "name": "remove"})
|
delete_button = modal.find("button", {"type": "submit", "name": "remove"})
|
||||||
self.assertIsNotNone(delete_button)
|
self.assertIsNotNone(delete_button)
|
||||||
self.assertEqual("Delete...", delete_button.text.strip())
|
self.assertEqual("Delete", delete_button.text.strip())
|
||||||
self.assertEqual(str(bookmark.id), delete_button["value"])
|
self.assertEqual(str(bookmark.id), delete_button["value"])
|
||||||
|
|
||||||
form = delete_button.find_parent("form")
|
form = delete_button.find_parent("form")
|
||||||
|
@@ -231,7 +231,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
f"""
|
f"""
|
||||||
<button type="submit" name="unshare" value="{bookmark.id}"
|
<button type="submit" name="unshare" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
|
ld-confirm-button ld-confirm-question="Unshare?">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
<use xlink:href="#ld-icon-share"></use>
|
<use xlink:href="#ld-icon-share"></use>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -247,7 +247,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
f"""
|
f"""
|
||||||
<button type="submit" name="mark_as_read" value="{bookmark.id}"
|
<button type="submit" name="mark_as_read" value="{bookmark.id}"
|
||||||
class="btn btn-link btn-sm btn-icon"
|
class="btn btn-link btn-sm btn-icon"
|
||||||
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
|
ld-confirm-button ld-confirm-question="Mark as read?">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||||
<use xlink:href="#ld-icon-unread"></use>
|
<use xlink:href="#ld-icon-unread"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@@ -140,8 +140,8 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
# Delete bookmark, verify return url
|
# Delete bookmark, verify return url
|
||||||
with self.page.expect_navigation(url=self.live_server_url + url):
|
with self.page.expect_navigation(url=self.live_server_url + url):
|
||||||
details_modal.get_by_text("Delete...").click()
|
details_modal.get_by_text("Delete").click()
|
||||||
details_modal.get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
# verify bookmark is deleted
|
# verify bookmark is deleted
|
||||||
self.locate_bookmark(bookmark.title)
|
self.locate_bookmark(bookmark.title)
|
||||||
@@ -173,7 +173,7 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
# Remove snapshot
|
# Remove snapshot
|
||||||
asset_list.get_by_text("Remove", exact=False).click()
|
asset_list.get_by_text("Remove", exact=False).click()
|
||||||
asset_list.get_by_text("Confirm", exact=False).click()
|
self.locate_confirm_dialog().get_by_text("Confirm", exact=False).click()
|
||||||
|
|
||||||
# Snapshot is removed
|
# Snapshot is removed
|
||||||
expect(snapshot).not_to_be_visible()
|
expect(snapshot).not_to_be_visible()
|
||||||
|
@@ -46,7 +46,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
# Execute bulk action
|
# Execute bulk action
|
||||||
self.select_bulk_action("Mark as unread")
|
self.select_bulk_action("Mark as unread")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
@@ -323,7 +323,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
# Wait until bookmark list is updated (old reference becomes invisible)
|
# Wait until bookmark list is updated (old reference becomes invisible)
|
||||||
expect(bookmark_list).not_to_be_visible()
|
expect(bookmark_list).not_to_be_visible()
|
||||||
|
|
||||||
|
@@ -123,7 +123,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.open(reverse("linkding:bookmarks.index"), p)
|
self.open(reverse("linkding:bookmarks.index"), p)
|
||||||
|
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
|
self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
@@ -140,7 +140,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
|
expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
|
self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
|
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
@@ -156,7 +156,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
|
expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
|
self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
|
||||||
self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
|
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
|
||||||
self.assertReloads(0)
|
self.assertReloads(0)
|
||||||
@@ -173,7 +173,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
).click()
|
).click()
|
||||||
self.select_bulk_action("Archive")
|
self.select_bulk_action("Archive")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
@@ -191,7 +191,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
).click()
|
).click()
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
|
||||||
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
self.assertVisibleTags(["Tag 1", "Tag 3"])
|
||||||
@@ -216,7 +216,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.open(reverse("linkding:bookmarks.archived"), p)
|
self.open(reverse("linkding:bookmarks.archived"), p)
|
||||||
|
|
||||||
self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
|
self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
|
||||||
self.locate_bookmark("Archived Bookmark 2").get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
@@ -234,7 +234,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
).click()
|
).click()
|
||||||
self.select_bulk_action("Unarchive")
|
self.select_bulk_action("Unarchive")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
@@ -252,7 +252,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
).click()
|
).click()
|
||||||
self.select_bulk_action("Delete")
|
self.select_bulk_action("Delete")
|
||||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||||
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
|
||||||
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
|
||||||
@@ -293,7 +293,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.open(reverse("linkding:bookmarks.shared"), p)
|
self.open(reverse("linkding:bookmarks.shared"), p)
|
||||||
|
|
||||||
self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
|
self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
|
||||||
self.locate_bookmark("My Bookmark 2").get_by_text("Confirm").click()
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
self.assertVisibleBookmarks(
|
self.assertVisibleBookmarks(
|
||||||
[
|
[
|
||||||
|
@@ -92,3 +92,6 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
|||||||
).click()
|
).click()
|
||||||
else:
|
else:
|
||||||
self.page.locator("nav").get_by_text(main_menu_item, exact=True).click()
|
self.page.locator("nav").get_by_text(main_menu_item, exact=True).click()
|
||||||
|
|
||||||
|
def locate_confirm_dialog(self):
|
||||||
|
return self.page.locator(".dropdown.confirm-dropdown")
|
||||||
|
Reference in New Issue
Block a user