Repository: sissbruecker/linkding
Pull request #975 (2025-02-02 10:22:48)

Add option to collapse side panel
Related to https://github.com/sissbruecker/linkding/issues/205


Patch:
diff --git a/bookmarks/migrations/0043_userprofile_collapse_side_panel.py b/bookmarks/migrations/0043_userprofile_collapse_side_panel.py
new file mode 100644
index 0000000..4a0405f
--- /dev/null
+++ b/bookmarks/migrations/0043_userprofile_collapse_side_panel.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.5 on 2025-02-02 09:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookmarks", "0042_userprofile_custom_css_hash"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="collapse_side_panel",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/bookmarks/models.py b/bookmarks/models.py
index fe4530b..38d8c30 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -440,6 +440,7 @@ class UserProfile(models.Model):
         null=False, default=30, validators=[MinValueValidator(10)]
     )
     sticky_pagination = models.BooleanField(default=False, null=False)
+    collapse_side_panel = models.BooleanField(default=False, null=False)
 
     def save(self, *args, **kwargs):
         if self.custom_css:
@@ -479,6 +480,7 @@ class UserProfileForm(forms.ModelForm):
             "auto_tagging_rules",
             "items_per_page",
             "sticky_pagination",
+            "collapse_side_panel",
         ]
 
 
diff --git a/bookmarks/styles/bookmark-details.css b/bookmarks/styles/bookmark-details.css
index ba860fb..5aa0215 100644
--- a/bookmarks/styles/bookmark-details.css
+++ b/bookmarks/styles/bookmark-details.css
@@ -36,7 +36,6 @@
     }
   }
 
-
   & .sections section {
     margin-top: var(--unit-4);
   }
diff --git a/bookmarks/styles/bookmark-page.css b/bookmarks/styles/bookmark-page.css
index dcefd83..45f81e3 100644
--- a/bookmarks/styles/bookmark-page.css
+++ b/bookmarks/styles/bookmark-page.css
@@ -10,8 +10,38 @@
 }
 
 /* Bookmark page grid */
-.bookmarks-page.grid {
-  grid-gap: var(--unit-9);
+.bookmarks-page {
+  &.grid {
+    grid-gap: var(--unit-9);
+  }
+
+  [ld-tag-modal-trigger] {
+    display: none;
+  }
+
+  @media (max-width: 840px) {
+    section.side-panel {
+      display: none;
+    }
+
+    [ld-tag-modal-trigger] {
+      display: inline-block;
+    }
+  }
+
+  &.collapse-side-panel {
+    section.main {
+      grid-column: span var(--grid-columns);
+    }
+
+    section.side-panel {
+      display: none;
+    }
+
+    [ld-tag-modal-trigger] {
+      display: inline-block;
+    }
+  }
 }
 
 /* Bookmark area header controls */
diff --git a/bookmarks/templates/bookmarks/archive.html b/bookmarks/templates/bookmarks/archive.html
index e3ab325..4ae8dee 100644
--- a/bookmarks/templates/bookmarks/archive.html
+++ b/bookmarks/templates/bookmarks/archive.html
@@ -4,16 +4,17 @@
 {% load bookmarks %}
 
 {% block content %}
-  <div ld-bulk-edit class="bookmarks-page grid columns-md-1">
+  <div ld-bulk-edit
+       class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="content-area col-2">
+    <section class="main content-area col-2">
       <div class="content-area-header mb-0">
         <h2>Archived bookmarks</h2>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search mode='archived' %}
           {% include 'bookmarks/bulk_edit/toggle.html' %}
-          <button ld-tag-modal-trigger class="btn ml-2 show-md">Tags
+          <button ld-tag-modal-trigger class="btn ml-2">Tags
           </button>
         </div>
       </div>
@@ -31,7 +32,7 @@
     </section>
 
     {# Tag cloud #}
-    <section class="content-area col-1 hide-md">
+    <section class="side-panel content-area col-1">
       <div class="content-area-header">
         <h2>Tags</h2>
       </div>
diff --git a/bookmarks/templates/bookmarks/index.html b/bookmarks/templates/bookmarks/index.html
index 44ad944..7cb6c9a 100644
--- a/bookmarks/templates/bookmarks/index.html
+++ b/bookmarks/templates/bookmarks/index.html
@@ -4,16 +4,17 @@
 {% load bookmarks %}
 
 {% block content %}
-  <div ld-bulk-edit class="bookmarks-page grid columns-md-1">
+  <div ld-bulk-edit
+       class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="content-area col-2">
+    <section class="main content-area col-2">
       <div class="content-area-header mb-0">
         <h2>Bookmarks</h2>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search %}
           {% include 'bookmarks/bulk_edit/toggle.html' %}
-          <button ld-tag-modal-trigger class="btn ml-2 show-md">Tags</button>
+          <button ld-tag-modal-trigger class="btn ml-2">Tags</button>
         </div>
       </div>
 
@@ -30,7 +31,7 @@
     </section>
 
     {# Tag cloud #}
-    <section class="content-area col-1 hide-md">
+    <section class="side-panel content-area col-1">
       <div class="content-area-header">
         <h2>Tags</h2>
       </div>
diff --git a/bookmarks/templates/bookmarks/shared.html b/bookmarks/templates/bookmarks/shared.html
index fc0577b..a9195de 100644
--- a/bookmarks/templates/bookmarks/shared.html
+++ b/bookmarks/templates/bookmarks/shared.html
@@ -4,15 +4,16 @@
 {% load bookmarks %}
 
 {% block content %}
-  <div class="bookmarks-page grid columns-md-1">
+  <div
+      class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
 
     {# Bookmark list #}
-    <section class="content-area col-2">
+    <section class="main content-area col-2">
       <div class="content-area-header">
         <h2>Shared bookmarks</h2>
         <div class="header-controls">
           {% bookmark_search bookmark_list.search mode='shared' %}
-          <button ld-tag-modal-trigger class="btn ml-2 show-md">Tags
+          <button ld-tag-modal-trigger class="btn ml-2">Tags
           </button>
         </div>
       </div>
@@ -28,7 +29,7 @@
     </section>
 
     {# Filters #}
-    <section class="content-area col-1 hide-md">
+    <section class="side-panel content-area col-1">
       <div class="content-area-header">
         <h2>User</h2>
       </div>
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index dc6f070..866d0fa 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -124,6 +124,16 @@
             visible without having to scroll to the end of the page first.
           </div>
         </div>
+        <div class="form-group">
+          <label for="{{ form.collapse_side_panel.id_for_label }}" class="form-checkbox">
+            {{ form.collapse_side_panel }}
+            <i class="form-icon"></i> Collapse tags
+          </label>
+          <div class="form-input-hint">
+            When enabled, the tags side panel will be collapsed by default to give more space to the bookmark list.
+            Instead, the tags can be shown in a modal dialog by clicking the tags button in the bookmark list header.
+          </div>
+        </div>
         <div class="form-group">
           <label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
           {{ form.tag_search|add_class:"form-select width-25 width-sm-100" }}
diff --git a/bookmarks/views/contexts.py b/bookmarks/views/contexts.py
index 7b50e17..6f53dae 100644
--- a/bookmarks/views/contexts.py
+++ b/bookmarks/views/contexts.py
@@ -208,6 +208,7 @@ class BookmarkListContext:
         self.show_favicons = user_profile.enable_favicons
         self.show_preview_images = user_profile.enable_preview_images
         self.show_notes = user_profile.permanent_notes
+        self.collapse_side_panel = user_profile.collapse_side_panel
 
     @staticmethod
     def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):


Test patch:
diff --git a/bookmarks/e2e/e2e_test_collapse_side_panel.py b/bookmarks/e2e/e2e_test_collapse_side_panel.py
new file mode 100644
index 0000000..407b4f7
--- /dev/null
+++ b/bookmarks/e2e/e2e_test_collapse_side_panel.py
@@ -0,0 +1,48 @@
+from django.urls import reverse
+from playwright.sync_api import sync_playwright, expect
+
+from bookmarks.e2e.helpers import LinkdingE2ETestCase
+
+
+class CollapseSidePanelE2ETestCase(LinkdingE2ETestCase):
+
+    def setUp(self) -> None:
+        super().setUp()
+
+    def assertSidePanelIsVisible(self):
+        expect(self.page.locator(".bookmarks-page .side-panel")).to_be_visible()
+        expect(
+            self.page.locator(".bookmarks-page [ld-tag-modal-trigger]")
+        ).not_to_be_visible()
+
+    def assertSidePanelIsHidden(self):
+        expect(self.page.locator(".bookmarks-page .side-panel")).not_to_be_visible()
+        expect(
+            self.page.locator(".bookmarks-page [ld-tag-modal-trigger]")
+        ).to_be_visible()
+
+    def test_side_panel_should_be_visible_by_default(self):
+        with sync_playwright() as p:
+            self.open(reverse("bookmarks:index"), p)
+            self.assertSidePanelIsVisible()
+
+            self.page.goto(self.live_server_url + reverse("bookmarks:archived"))
+            self.assertSidePanelIsVisible()
+
+            self.page.goto(self.live_server_url + reverse("bookmarks:shared"))
+            self.assertSidePanelIsVisible()
+
+    def test_side_panel_should_be_hidden_when_collapsed(self):
+        user = self.get_or_create_test_user()
+        user.profile.collapse_side_panel = True
+        user.profile.save()
+
+        with sync_playwright() as p:
+            self.open(reverse("bookmarks:index"), p)
+            self.assertSidePanelIsHidden()
+
+            self.page.goto(self.live_server_url + reverse("bookmarks:archived"))
+            self.assertSidePanelIsHidden()
+
+            self.page.goto(self.live_server_url + reverse("bookmarks:shared"))
+            self.assertSidePanelIsHidden()
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index 10f5cf7..1492907 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -47,6 +47,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "auto_tagging_rules": "",
             "items_per_page": "30",
             "sticky_pagination": False,
+            "collapse_side_panel": False,
         }
 
         return {**form_data, **overrides}
@@ -117,6 +118,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "auto_tagging_rules": "example.com tag",
             "items_per_page": "10",
             "sticky_pagination": True,
+            "collapse_side_panel": True,
         }
         response = self.client.post(
             reverse("bookmarks:settings.update"), form_data, follow=True
@@ -194,6 +196,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
         self.assertEqual(
             self.user.profile.sticky_pagination, form_data["sticky_pagination"]
         )
+        self.assertEqual(
+            self.user.profile.collapse_side_panel, form_data["collapse_side_panel"]
+        )
 
         self.assertSuccessMessage(html, "Profile updated")