Avoid page reload when triggering actions in bookmark list (#506)

* Extract bookmark view contexts

* Implement basic partial updates for bookmark list and tag cloud

* Refactor confirm button JS into web component

* Refactor bulk edit JS into web component

* Refactor tag autocomplete JS into web component

* Refactor bookmark page JS into web component

* Refactor global shortcuts JS into web component

* Update tests

* Add E2E test for partial updates

* Add partial updates for archived bookmarks

* Add partial updates for shared bookmarks

* Cleanup helpers

* Improve naming in bulk edit

* Refactor shared components into behaviors

* Refactor bulk edit components into behaviors

* Refactor bookmark list components into behaviors

* Update tests

* Combine all scripts into bundle

* Fix E2E CI
This commit is contained in:
Sascha Ißbrücker
2023-08-21 23:12:00 +02:00
committed by GitHub
parent 8206705876
commit be789ea9e6
54 changed files with 1942 additions and 1388 deletions

View File

@@ -4,43 +4,42 @@
{% load bookmarks %}
{% block content %}
{% include 'bookmarks/bulk_edit/state.html' %}
<div class="bookmarks-page columns">
<div class="bookmarks-page columns"
ld-bulk-edit
ld-bookmark-page
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.archived' %}"
tags-url="{% url 'bookmarks:partials.tag_cloud.archived' %}">
{# Bookmark list #}
<section class="content-area column col-8 col-md-12">
<div class="content-area-header">
<h2>Archived bookmarks</h2>
<div class="spacer"></div>
{% bookmark_search filters tags mode='archived' %}
{% bookmark_search bookmark_list.filters tag_cloud.tags mode='archived' %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
</div>
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ return_url }}"
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ bookmark_list.return_url }}"
method="post">
{% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with mode='archive' %}
{% if empty %}
{% include 'bookmarks/empty_bookmarks.html' %}
{% else %}
{% bookmark_list bookmarks return_url link_target %}
{% endif %}
<div class="bookmark-list-container">
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
{# Tag list #}
{# Tag cloud #}
<section class="content-area column col-4 hide-md">
<div class="content-area-header">
<h2>Tags</h2>
</div>
{% tag_cloud tags selected_tags %}
<div class="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
<script src="{% static "bundle.js" %}"></script>
<script src="{% static "shared.js" %}"></script>
<script src="{% static "bookmark_list.js" %}"></script>
{% endblock %}

View File

@@ -1,128 +1,136 @@
{% load static %}
{% load shared %}
{% load pagination %}
<ul class="bookmark-list{% if request.user_profile.permanent_notes %} show-notes{% endif %}">
{% for bookmark in bookmarks %}
<li data-is-bookmark-item>
<label class="form-checkbox bulk-edit-toggle">
<input type="checkbox" name="bookmark_id" value="{{ bookmark.id }}">
<i class="form-icon"></i>
</label>
<div class="title">
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
class="{% if bookmark.unread %}text-italic{% endif %}">
{% if bookmark.favicon_file and request.user_profile.enable_favicons %}
<img src="{% static bookmark.favicon_file %}" alt="">
{% endif %}
{{ bookmark.resolved_title }}
</a>
</div>
{% if request.user_profile.display_url %}
<div class="url-path truncate">
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
class="url-display text-sm">
{{ bookmark.url }}
{% if bookmark_list.is_empty %}
{% include 'bookmarks/empty_bookmarks.html' %}
{% else %}
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}">
{% for bookmark in bookmark_list.bookmarks_page %}
<li ld-bookmark-item>
<label ld-bulk-edit-checkbox class="form-checkbox">
<input type="checkbox" name="bookmark_id" value="{{ bookmark.id }}">
<i class="form-icon"></i>
</label>
<div class="title">
<a href="{{ bookmark.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="{% if bookmark.unread %}text-italic{% endif %}">
{% if bookmark.favicon_file and bookmark_list.show_favicons %}
<img src="{% static bookmark.favicon_file %}" alt="">
{% endif %}
{{ bookmark.resolved_title }}
</a>
</div>
{% endif %}
<div class="description truncate">
{% if bookmark.tag_names %}
<span>
{% if bookmark_list.show_url %}
<div class="url-path truncate">
<a href="{{ bookmark.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="url-display text-sm">
{{ bookmark.url }}
</a>
</div>
{% endif %}
<div class="description truncate">
{% if bookmark.tag_names %}
<span>
{% for tag_name in bookmark.tag_names %}
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
{% endfor %}
</span>
{% endif %}
{% if bookmark.tag_names and bookmark.resolved_description %} | {% endif %}
{% if bookmark.resolved_description %}
<span>{{ bookmark.resolved_description }}</span>
{% endif %}
</div>
{% if bookmark.notes %}
<div class="notes bg-gray text-gray-dark">
<div class="notes-content">
{% markdown bookmark.notes %}
</div>
{% endif %}
{% if bookmark.tag_names and bookmark.resolved_description %} | {% endif %}
{% if bookmark.resolved_description %}
<span>{{ bookmark.resolved_description }}</span>
{% endif %}
</div>
{% endif %}
<div class="actions text-gray text-sm">
{% if request.user_profile.bookmark_date_display == 'relative' %}
<span>
{% if bookmark.notes %}
<div class="notes bg-gray text-gray-dark">
<div class="notes-content">
{% markdown bookmark.notes %}
</div>
</div>
{% endif %}
<div class="actions text-gray text-sm">
{% if bookmark_list.date_display == 'relative' %}
<span>
{% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}"
rel="noopener">
{% endif %}
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
</a>
{% endif %}
</span>
<span class="separator">|</span>
{% endif %}
{% if request.user_profile.bookmark_date_display == 'absolute' %}
<span>
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
rel="noopener">
</a>
{% endif %}
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
</a>
{% endif %}
</span>
<span class="separator">|</span>
{% endif %}
{% if bookmark.owner == request.user %}
{# Bookmark owner actions #}
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}">Edit</a>
{% if bookmark.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Unarchive
</button>
{% else %}
<button type="submit" name="archive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Archive
</button>
{% endif %}
<button type="submit" name="remove" value="{{ bookmark.id }}"
class="btn btn-link btn-sm btn-confirmation">Remove
</button>
{% if bookmark.unread %}
<span class="separator">|</span>
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Mark as read
</button>
{% endif %}
{% else %}
{# Shared bookmark actions #}
<span>Shared by
{% if bookmark_list.date_display == 'absolute' %}
<span>
{% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}"
rel="noopener">
{% endif %}
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
</a>
{% endif %}
</span>
<span class="separator">|</span>
{% endif %}
{% if bookmark.owner == request.user %}
{# Bookmark owner actions #}
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ bookmark_list.return_url }}">Edit</a>
{% if bookmark.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Unarchive
</button>
{% else %}
<button type="submit" name="archive" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Archive
</button>
{% endif %}
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Remove
</button>
{% if bookmark.unread %}
<span class="separator">|</span>
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
class="btn btn-link btn-sm">Mark as read
</button>
{% endif %}
{% else %}
{# Shared bookmark actions #}
<span>Shared by
<a href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
</span>
{% endif %}
{% if bookmark.notes and not request.user_profile.permanent_notes %}
<span class="separator">|</span>
<button class="btn btn-link btn-sm toggle-notes" title="Toggle notes">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16" height="16"
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="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
<path d="M9 7l6 0"></path>
<path d="M9 11l6 0"></path>
<path d="M9 15l4 0"></path>
</svg>
<span>Notes</span>
</button>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% if bookmark.notes and not bookmark_list.show_notes %}
<span class="separator">|</span>
<button class="btn btn-link btn-sm toggle-notes" title="Toggle notes">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16"
height="16"
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="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
<path d="M9 7l6 0"></path>
<path d="M9 11l6 0"></path>
<path d="M9 15l4 0"></path>
</svg>
<span>Notes</span>
</button>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
<div class="bookmark-pagination">
{% pagination bookmarks %}
</div>
<div class="bookmark-pagination">
{% pagination bookmark_list.bookmarks_page %}
</div>
{% endif %}

View File

@@ -1,34 +1,34 @@
{% load shared %}
{% htmlmin %}
<div class="bulk-edit-bar">
<div class="bulk-edit-actions bg-gray">
<label class="form-checkbox bulk-edit-all-toggle">
<input type="checkbox" style="display: none">
<i class="form-icon"></i>
</label>
{% if mode == 'archive' %}
<button type="submit" name="bulk_unarchive" class="btn btn-link btn-sm btn-confirmation"
title="Unarchive selected bookmarks">Unarchive
<div class="bulk-edit-bar">
<div class="bulk-edit-actions bg-gray">
<label ld-bulk-edit-checkbox all class="form-checkbox">
<input type="checkbox" style="display: none">
<i class="form-icon"></i>
</label>
{% if mode == 'archive' %}
<button ld-confirm-button type="submit" name="bulk_unarchive" class="btn btn-link btn-sm"
title="Unarchive selected bookmarks">Unarchive
</button>
{% else %}
<button ld-confirm-button type="submit" name="bulk_archive" class="btn btn-link btn-sm"
title="Archive selected bookmarks">Archive
</button>
{% endif %}
<span class="text-sm text-gray-dark"></span>
<button ld-confirm-button type="submit" name="bulk_delete" class="btn btn-link btn-sm"
title="Delete selected bookmarks">Delete
</button>
{% else %}
<button type="submit" name="bulk_archive" class="btn btn-link btn-sm btn-confirmation"
title="Archive selected bookmarks">Archive
<span class="text-sm text-gray-dark"></span>
<span class="text-sm text-gray-dark"><label for="bulk-edit-tags-input">Tags:</label></span>
<input ld-tag-autocomplete variant="small"
name="bulk_tag_string" class="form-input input-sm" placeholder="&nbsp;">
<button type="submit" name="bulk_tag" class="btn btn-link btn-sm"
title="Add tags to selected bookmarks">Add
</button>
{% endif %}
<span class="text-sm text-gray-dark"></span>
<button type="submit" name="bulk_delete" class="btn btn-link btn-sm btn-confirmation"
title="Delete selected bookmarks">Delete
</button>
<span class="text-sm text-gray-dark"></span>
<span class="text-sm text-gray-dark"><label for="bulk-edit-tags-input">Tags:</label></span>
<input id="bulk-edit-tags-input" name="bulk_tag_string" class="form-input input-sm"
placeholder="&nbsp;">
<button type="submit" name="bulk_tag" class="btn btn-link btn-sm"
title="Add tags to selected bookmarks">Add
</button>
<button type="submit" name="bulk_untag" class="btn btn-link btn-sm"
title="Remove tags from selected bookmarks">Remove
</button>
<button type="submit" name="bulk_untag" class="btn btn-link btn-sm"
title="Remove tags from selected bookmarks">Remove
</button>
</div>
</div>
</div>
{% endhtmlmin %}

View File

@@ -1 +0,0 @@
<input id="bulk-edit-mode" type="checkbox">

View File

@@ -1,9 +1,7 @@
<label for="bulk-edit-mode" class="hide-sm">
<span class="btn" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="20px"
height="20px">
<path
d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
</svg>
</span>
</label>
<button ld-bulk-edit-active-toggle class="btn hide-sm ml-2" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="20px"
height="20px">
<path
d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/>
</svg>
</button>

View File

@@ -21,7 +21,7 @@
</div>
<div class="form-group">
<label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label>
{{ form.tag_string|add_class:"form-input"|attr:"autocomplete:off"|attr:"autocapitalize:off" }}
{{ form.tag_string|add_class:"form-input"|attr:"ld-tag-autocomplete"|attr:"autocomplete:off"|attr:"autocapitalize:off" }}
<div class="form-input-hint">
Enter any number of tags separated by space and <strong>without</strong> the hash (#). If a tag does not
exist it will be
@@ -118,23 +118,6 @@
{# Replace tag input with auto-complete component #}
<script src="{% static "bundle.js" %}"></script>
<script type="application/javascript">
const wrapper = document.createElement('div');
const tagInput = document.getElementById('{{ form.tag_string.id_for_label }}');
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
new linkding.TagAutoComplete({
target: wrapper,
props: {
id: '{{ form.tag_string.id_for_label }}',
name: '{{ form.tag_string.name }}',
value: tagInput.value,
apiClient: apiClient
}
});
tagInput.parentElement.replaceChild(wrapper, tagInput);
</script>
<script type="application/javascript">
/**
* - Pre-fill title and description placeholders with metadata from website as soon as URL changes

View File

@@ -4,43 +4,42 @@
{% load bookmarks %}
{% block content %}
{% include 'bookmarks/bulk_edit/state.html' %}
<div class="bookmarks-page columns">
<div class="bookmarks-page columns"
ld-bulk-edit
ld-bookmark-page
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.active' %}"
tags-url="{% url 'bookmarks:partials.tag_cloud.active' %}">
{# Bookmark list #}
<section class="content-area column col-8 col-md-12">
<div class="content-area-header">
<h2>Bookmarks</h2>
<div class="spacer"></div>
{% bookmark_search filters tags %}
{% bookmark_search bookmark_list.filters tag_cloud.tags %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
</div>
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ return_url }}"
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ bookmark_list.return_url }}"
method="post">
{% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with mode='default' %}
{% if empty %}
{% include 'bookmarks/empty_bookmarks.html' %}
{% else %}
{% bookmark_list bookmarks return_url link_target %}
{% endif %}
<div class="bookmark-list-container">
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
{# Tag list #}
{# Tag cloud #}
<section class="content-area column col-4 hide-md">
<div class="content-area-header">
<h2>Tags</h2>
</div>
{% tag_cloud tags selected_tags %}
<div class="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
<script src="{% static "bundle.js" %}"></script>
<script src="{% static "shared.js" %}"></script>
<script src="{% static "bookmark_list.js" %}"></script>
{% endblock %}

View File

@@ -29,7 +29,7 @@
media="(prefers-color-scheme: light)"/>
{% endif %}
</head>
<body>
<body ld-global-shortcuts>
<header>
{% if has_toasts %}
<div class="toasts container grid-lg">

View File

@@ -4,26 +4,26 @@
{% load bookmarks %}
{% block content %}
<div class="bookmarks-page columns">
<div class="bookmarks-page columns"
ld-bookmark-page
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.shared' %}"
tags-url="{% url 'bookmarks:partials.tag_cloud.shared' %}">
{# Bookmark list #}
<section class="content-area column col-8 col-md-12">
<div class="content-area-header">
<h2>Shared bookmarks</h2>
<div class="spacer"></div>
{% bookmark_search filters tags mode='shared' %}
{% bookmark_search bookmark_list.filters tag_cloud.tags mode='shared' %}
</div>
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ return_url }}"
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ bookmark_list.return_url }}"
method="post">
{% csrf_token %}
{% if empty %}
{% include 'bookmarks/empty_bookmarks.html' %}
{% else %}
{% bookmark_list bookmarks return_url link_target %}
{% endif %}
<div class="bookmark-list-container">
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
@@ -39,11 +39,11 @@
<div class="content-area-header">
<h2>Tags</h2>
</div>
{% tag_cloud tags selected_tags %}
<div class="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
<script src="{% static "bundle.js" %}"></script>
<script src="{% static "shared.js" %}"></script>
<script src="{% static "bookmark_list.js" %}"></script>
{% endblock %}

View File

@@ -1,9 +1,9 @@
{% load shared %}
{% htmlmin %}
<div class="tag-cloud">
{% if has_selected_tags %}
{% if tag_cloud.has_selected_tags %}
<p class="selected-tags">
{% for tag in selected_tags %}
{% for tag in tag_cloud.selected_tags %}
<a href="?{% remove_tag_from_query tag.name %}"
class="text-bold mr-2">
<span>-{{ tag.name }}</span>
@@ -12,7 +12,7 @@
</p>
{% endif %}
<div class="unselected-tags">
{% for group in groups %}
{% for group in tag_cloud.groups %}
<p class="group">
{% for tag in group.tags %}
{# Highlight first char of first tag in group #}