Repository: supakeen/pinnwand (language: python)

Issue:
The first paste input field doesn't have the Remove This File button is there are a multiple paste inputs
**Steps to reproduce:**
1. Open the Pinnwand
2. Click the Add Another File button a few times
3. Try to delete the first paste input

**Actual result:** the first paste input can't be deleted as the Remove This File button is absent 
![20](https://github.com/supakeen/pinnwand/assets/49349130/eb581631-064c-4c08-9aa6-b44e05fc9ce7)

**Expected result:** the Remove This File button should be displayed for all the inputs if there are multiple paste inputs displayed on the page (to be able to delete the first paste and only paste the 2d and 3d ones if needed)


Discussion / hints:
This might also be the time to think about adding confirmation if a file is removed that already has content in its textarea.
Also if you're in this area then:

![image](https://github.com/supakeen/pinnwand/assets/39536295/e2a2d674-1577-4222-afae-2dca989edeb0)

There's a difference in alignment between the 'remove this file' and 'add another file' if you could pick that up too that'd be cool.

Fix patch:
diff --git a/src/asset/pinnwand.scss b/src/asset/pinnwand.scss
index 4523fb1..179dc15 100644
--- a/src/asset/pinnwand.scss
+++ b/src/asset/pinnwand.scss
@@ -177,7 +177,8 @@ section.paste-submit {
 }
 
 div.file-meta button,
-section.paste-submit button {
+section.paste-submit button,
+button.confirm {
     box-sizing: border-box;
     border: .0625rem solid var(--color3);
     padding: .4rem 1.4rem;
@@ -190,12 +191,42 @@ section.paste-submit button {
 }
 
 div.file-meta button:hover,
-section.paste-submit button:hover {
+section.paste-submit button:hover,
+button.confirm:hover {
     color: var(--color3);
     background: var(--color1);
     border: .0625rem solid var(--color3);
 }
 
+button.confirm:hover {
+    color: var(--color3);
+    background: var(--color1);
+    border: .0625rem solid var(--color3);
+}
+
+.action-buttons {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+button.cancel {
+    box-sizing: border-box;
+    border: .0625rem solid var(--color3);
+    padding: .4rem 1.4rem;
+    margin: 0 .2rem;
+    background: var(--color1);
+    color: var(--color3);
+    cursor: pointer;
+    font-size: 1.1rem;
+    text-align: center;
+}
+
+button.cancel:hover {
+    color: var(--color1);
+    background: var(--color3);
+    border: .0625rem solid var(--color3);
+}
 
 article, div.paste-meta {
     background-color: var(--color1);
@@ -406,6 +437,19 @@ textarea.copy-area {
     background-color: rgba(255,223,93,0.2);
 }
 
+.confirmation-popover {
+    padding: 2rem;
+    border: .0625rem solid var(--color3);
+    background-color: var(--color0);
+    color: var(--color2);
+    position: fixed;
+    overflow: auto;
+    width: fit-content;
+    height: fit-content;
+    margin: auto;
+    inset: 0;
+}
+
 .hll { background-color: #ffffcc }
 .c { color: #408080; font-style: italic } /* Comment */
 .err { border: 1px solid #FF0000 } /* Error */
diff --git a/src/pinnwand/static/pinnwand.css b/src/pinnwand/static/pinnwand.css
index ee27e7d..e124642 100644
Binary files a/src/pinnwand/static/pinnwand.css and b/src/pinnwand/static/pinnwand.css differ
diff --git a/src/pinnwand/static/pinnwand.js b/src/pinnwand/static/pinnwand.js
index aac8b7d..2befed6 100644
--- a/src/pinnwand/static/pinnwand.js
+++ b/src/pinnwand/static/pinnwand.js
@@ -46,7 +46,12 @@ function addRemoveButtons() {
 
     const main = document.querySelector("main.page-create");
 
-    document.querySelectorAll('.file-part .file-meta').forEach(el => {
+    const fileParts = document.querySelectorAll('.file-part .file-meta');
+    if (fileParts.length < 2) {
+        return;
+    }
+
+    fileParts.forEach(el => {
         if (el.querySelector(selector)) {
             return;
         }
@@ -55,17 +60,29 @@ function addRemoveButtons() {
         removeButton.className = className;
         el.appendChild(removeButton);
 
-        removeButton.addEventListener("click", (event) => {
-            event.preventDefault();
-            const section = event.target.parentNode.parentNode;
+        const removeSection = (section) => {
             main.removeChild(section);
-
-            const fileInputs = document.querySelectorAll("section.file-part");
-
             const fileParts = document.querySelectorAll('.file-part .file-meta');
-
             if (fileParts.length < 2) {
-               document.querySelector(selector)?.remove();
+                document.querySelector(selector)?.remove();
+            }
+        };
+
+        removeButton.addEventListener("click", (event) => {
+            event.preventDefault();
+            const section = event.target.parentNode.parentNode;
+            const sectionContent = section.querySelector('textarea').value;
+            if (sectionContent) {
+                const popover = document.querySelector(".confirmation-popover");
+                popover.removeAttribute("hidden");
+                popover.querySelector(".confirm").focus();
+                popover.querySelector(".cancel").addEventListener("click", (event) => event.target.parentNode.parentNode.setAttribute("hidden", true));
+                popover.querySelector(".confirm").addEventListener("click", (event) => {
+                    event.target.parentNode.parentNode.setAttribute("hidden", true);
+                    removeSection(section);
+                });
+            } else {
+                removeSection(section);
             }
         });
     });
diff --git a/src/pinnwand/template/layout.html b/src/pinnwand/template/layout.html
index 0853a64..42c2732 100644
--- a/src/pinnwand/template/layout.html
+++ b/src/pinnwand/template/layout.html
@@ -21,5 +21,12 @@
       {% raw handler.application.configuration.footer %}
       <button class="btn-link" id="toggle-color-scheme">Toggle color scheme</button>.
     </footer>
+    <div hidden class="confirmation-popover">
+      <h2>Are you sure you'd like to remove this file?</h2>
+      <div class="action-buttons">
+        <button class="confirm">Confirm</button>
+        <button class="cancel">Cancel</button>
+      </div>
+    </div>
   </body>
 </html>


Test patch:
diff --git a/test/e2e/pageobjects/create_paste_page.py b/test/e2e/pageobjects/create_paste_page.py
index 8c473a6..5c52787 100644
--- a/test/e2e/pageobjects/create_paste_page.py
+++ b/test/e2e/pageobjects/create_paste_page.py
@@ -25,6 +25,7 @@ def __init__(self, page: Page) -> None:
         self.filename_input = page.locator("input[name=filename]")
         self.filetype_select = page.locator("select[name=lexer]")
         self.selected_option = page.locator("option[selected]")
+        self.removal_confirmation_modal = RemovalConfirmationModal(page)
 
     def open(self):
         log.info(f"Opening Pinnwand at {self.url}")
@@ -154,3 +155,35 @@ def should_have_selected_filetype(self, filetype, paste_number=0):
             self.selected_option.nth(paste_number).text_content() == filetype,
             f"Incorrect option was selected in Filetype Select on {self.page_name}",
         )
+
+    def should_have_paste_inputs(self, number_of_paste_inputs):
+        assert (
+            len(self.paste_input.all()) == number_of_paste_inputs,
+            f"{self.page_name} had incorrect number of Paste Inputs",
+        )
+
+
+class RemovalConfirmationModal:
+    def __init__(self, page: Page) -> None:
+        self.page = page
+        self.modal_locator = page.locator(".confirmation-popover")
+        self.confirm_button = page.locator(".confirm")
+        self.cancel_button = page.locator(".cancel")
+
+    def confirm(self):
+        log.info("Clicking Confirm button")
+        self.confirm_button.click()
+
+    def cancel(self):
+        log.info("Clicking Cancel button")
+        self.cancel_button.click()
+
+    def should_be_displayed(self):
+        expect(
+            self.modal_locator, f"Removal Confirmation modal was not displayed"
+        ).to_be_visible()
+
+    def should_not_be_displayed(self):
+        expect(
+            self.modal_locator, f"Removal Confirmation modal was displayed"
+        ).not_to_be_visible()
diff --git a/test/e2e/pinnwand.toml b/test/e2e/pinnwand.toml
index 7de924d..bc77eb4 100644
--- a/test/e2e/pinnwand.toml
+++ b/test/e2e/pinnwand.toml
@@ -1,4 +1,4 @@
-database_uri = "sqlite:///C:\\Users\\peeka\\AppData\\Local\\Temp\\tmpjctuctpq"
+database_uri = "sqlite:///C:\\Users\\peeka\\AppData\\Local\\Temp\\tmpdxas7neb"
 
 [ratelimit.read]
 capacity = 200
diff --git a/test/e2e/testscenarios/test_file_removal_confirmation.py b/test/e2e/testscenarios/test_file_removal_confirmation.py
new file mode 100644
index 0000000..0f21de2
--- /dev/null
+++ b/test/e2e/testscenarios/test_file_removal_confirmation.py
@@ -0,0 +1,54 @@
+import pytest
+from test.e2e.pageobjects.create_paste_page import CreatePastePage
+
+
+@pytest.mark.e2e
+def test_confirm_removal(create_paste_page: CreatePastePage):
+    create_paste_page.paste_random_text(paste_number=0)
+    create_paste_page.click_add_another_file_button()
+
+    second_pasted_text = create_paste_page.paste_random_text(paste_number=1)
+
+    create_paste_page.click_remove_file_button(paste_number=0)
+    create_paste_page.removal_confirmation_modal.should_be_displayed()
+
+    create_paste_page.removal_confirmation_modal.confirm()
+    create_paste_page.removal_confirmation_modal.should_not_be_displayed()
+    create_paste_page.should_have_paste_inputs(1)
+    create_paste_page.should_have_value_in_paste_input(
+        second_pasted_text, paste_number=0
+    )
+
+
+@pytest.mark.e2e
+def test_cancel_removal(create_paste_page: CreatePastePage):
+    first_pasted_text = create_paste_page.paste_random_text(paste_number=0)
+    create_paste_page.click_add_another_file_button()
+    second_pasted_text = create_paste_page.paste_random_text(paste_number=1)
+
+    create_paste_page.click_remove_file_button(paste_number=0)
+    create_paste_page.removal_confirmation_modal.should_be_displayed()
+
+    create_paste_page.removal_confirmation_modal.cancel()
+    create_paste_page.removal_confirmation_modal.should_not_be_displayed()
+    create_paste_page.should_have_paste_inputs(2)
+    create_paste_page.should_have_value_in_paste_input(
+        first_pasted_text, paste_number=0
+    )
+    create_paste_page.should_have_value_in_paste_input(
+        second_pasted_text, paste_number=1
+    )
+
+
+@pytest.mark.e2e
+def test_removal_of_empty_paste(create_paste_page: CreatePastePage):
+    first_pasted_text = create_paste_page.paste_random_text(paste_number=0)
+    create_paste_page.click_add_another_file_button()
+
+    create_paste_page.click_remove_file_button(paste_number=1)
+    create_paste_page.removal_confirmation_modal.should_not_be_displayed()
+
+    create_paste_page.should_have_paste_inputs(1)
+    create_paste_page.should_have_value_in_paste_input(
+        first_pasted_text, paste_number=0
+    )
diff --git a/test/e2e/testscenarios/test_remove_file.py b/test/e2e/testscenarios/test_remove_file.py
index f9bf0e5..c8097ff 100644
--- a/test/e2e/testscenarios/test_remove_file.py
+++ b/test/e2e/testscenarios/test_remove_file.py
@@ -16,6 +16,8 @@ def test_remove_file(page: Page, create_paste_page: CreatePastePage):
     third_pasted_text = create_paste_page.paste_random_text(paste_number=2)
 
     create_paste_page.click_remove_file_button(paste_number=1)
+    create_paste_page.removal_confirmation_modal.confirm()
+
     create_paste_page.should_have_value_in_paste_input(
         first_pasted_text, paste_number=0
     )
@@ -62,6 +64,8 @@ def test_remove_file_while_repasting(
     create_paste_page.should_be_opened()
 
     create_paste_page.click_remove_file_button(paste_number=2)
+    create_paste_page.removal_confirmation_modal.confirm()
+
     create_paste_page.should_not_have_value_in_paste_input(third_file_content)
     create_paste_page.click_submit()
 
@@ -87,6 +91,7 @@ def test_remove_first_file(page: Page, create_paste_page: CreatePastePage):
         )
 
     create_paste_page.click_remove_file_button(paste_number=0)
+    create_paste_page.removal_confirmation_modal.confirm()
 
     create_paste_page.should_have_value_in_paste_input(
         second_file_content, paste_number=0
@@ -123,6 +128,7 @@ def test_remove_first_file_while_repasting(
     create_paste_page.should_be_opened()
 
     create_paste_page.click_remove_file_button(paste_number=0)
+    create_paste_page.removal_confirmation_modal.confirm()
     create_paste_page.should_not_have_value_in_paste_input(first_pasted_text)
     create_paste_page.should_have_value_in_paste_input(
         second_pasted_text, paste_number=0
diff --git a/test/e2e/utils/string_utils.py b/test/e2e/utils/string_utils.py
index 678cdf2..68f15c1 100644
--- a/test/e2e/utils/string_utils.py
+++ b/test/e2e/utils/string_utils.py
@@ -3,7 +3,7 @@
 
 
 def random_string(size=1000) -> str:
-    return random.choice(string.ascii_letters).join(
+    return random.choice(string.ascii_letters) + "".join(
         [
             random.choice(
                 string.ascii_letters