diff --git a/bookmarks/forms.py b/bookmarks/forms.py index da58d0a..d5dd032 100644 --- a/bookmarks/forms.py +++ b/bookmarks/forms.py @@ -1,10 +1,15 @@ from django import forms -from django.forms.utils import ErrorList +from django.contrib.auth.models import User +from django.db import models from django.utils import timezone from bookmarks.models import ( Bookmark, + BookmarkBundle, + BookmarkSearch, + GlobalSettings, Tag, + UserProfile, build_tag_string, parse_tag_string, sanitize_tag_name, @@ -12,23 +17,29 @@ from bookmarks.models import ( from bookmarks.services.bookmarks import create_bookmark, update_bookmark from bookmarks.type_defs import HttpRequest from bookmarks.validators import BookmarkURLValidator - - -class CustomErrorList(ErrorList): - template_name = "shared/error_list.html" +from bookmarks.widgets import ( + FormCheckbox, + FormErrorList, + FormInput, + FormNumberInput, + FormSelect, + FormTextarea, + TagAutocomplete, +) class BookmarkForm(forms.ModelForm): # Use URLField for URL - url = forms.CharField(validators=[BookmarkURLValidator()]) - tag_string = forms.CharField(required=False) + url = forms.CharField(validators=[BookmarkURLValidator()], widget=FormInput) + tag_string = forms.CharField(required=False, widget=TagAutocomplete) # Do not require title and description as they may be empty - title = forms.CharField(max_length=512, required=False) - description = forms.CharField(required=False, widget=forms.Textarea()) - unread = forms.BooleanField(required=False) - shared = forms.BooleanField(required=False) + title = forms.CharField(max_length=512, required=False, widget=FormInput) + description = forms.CharField(required=False, widget=FormTextarea) + notes = forms.CharField(required=False, widget=FormTextarea) + unread = forms.BooleanField(required=False, widget=FormCheckbox) + shared = forms.BooleanField(required=False, widget=FormCheckbox) # Hidden field that determines whether to close window/tab after saving the bookmark - auto_close = forms.CharField(required=False) + auto_close = forms.CharField(required=False, widget=forms.HiddenInput) class Meta: model = Bookmark @@ -62,7 +73,7 @@ class BookmarkForm(forms.ModelForm): initial = {"tag_string": build_tag_string(instance.tag_names, " ")} data = request.POST if request.method == "POST" else None super().__init__( - data, instance=instance, initial=initial, error_class=CustomErrorList + data, instance=instance, initial=initial, error_class=FormErrorList ) @property @@ -111,12 +122,14 @@ def convert_tag_string(tag_string: str): class TagForm(forms.ModelForm): + name = forms.CharField(widget=FormInput) + class Meta: model = Tag fields = ["name"] def __init__(self, user, *args, **kwargs): - super().__init__(*args, **kwargs, error_class=CustomErrorList) + super().__init__(*args, **kwargs, error_class=FormErrorList) self.user = user def clean_name(self): @@ -146,11 +159,11 @@ class TagForm(forms.ModelForm): class TagMergeForm(forms.Form): - target_tag = forms.CharField() - merge_tags = forms.CharField() + target_tag = forms.CharField(widget=TagAutocomplete) + merge_tags = forms.CharField(widget=TagAutocomplete) def __init__(self, user, *args, **kwargs): - super().__init__(*args, **kwargs, error_class=CustomErrorList) + super().__init__(*args, **kwargs, error_class=FormErrorList) self.user = user def clean_target_tag(self): @@ -197,3 +210,156 @@ class TagMergeForm(forms.Form): ) return merge_tags + + +class BookmarkBundleForm(forms.ModelForm): + name = forms.CharField(max_length=256, widget=FormInput) + search = forms.CharField(max_length=256, required=False, widget=FormInput) + any_tags = forms.CharField(required=False, widget=TagAutocomplete) + all_tags = forms.CharField(required=False, widget=TagAutocomplete) + excluded_tags = forms.CharField(required=False, widget=TagAutocomplete) + + class Meta: + model = BookmarkBundle + fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, error_class=FormErrorList) + + +class BookmarkSearchForm(forms.Form): + SORT_CHOICES = [ + (BookmarkSearch.SORT_ADDED_ASC, "Added ↑"), + (BookmarkSearch.SORT_ADDED_DESC, "Added ↓"), + (BookmarkSearch.SORT_TITLE_ASC, "Title ↑"), + (BookmarkSearch.SORT_TITLE_DESC, "Title ↓"), + ] + FILTER_SHARED_CHOICES = [ + (BookmarkSearch.FILTER_SHARED_OFF, "Off"), + (BookmarkSearch.FILTER_SHARED_SHARED, "Shared"), + (BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"), + ] + FILTER_UNREAD_CHOICES = [ + (BookmarkSearch.FILTER_UNREAD_OFF, "Off"), + (BookmarkSearch.FILTER_UNREAD_YES, "Unread"), + (BookmarkSearch.FILTER_UNREAD_NO, "Read"), + ] + + q = forms.CharField() + user = forms.ChoiceField(required=False, widget=FormSelect) + bundle = forms.CharField(required=False) + sort = forms.ChoiceField(choices=SORT_CHOICES, widget=FormSelect) + shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect) + unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect) + modified_since = forms.CharField(required=False) + added_since = forms.CharField(required=False) + + def __init__( + self, + search: BookmarkSearch, + editable_fields: list[str] = None, + users: list[User] = None, + ): + super().__init__() + editable_fields = editable_fields or [] + self.editable_fields = editable_fields + + # set choices for user field if users are provided + if users: + user_choices = [(user.username, user.username) for user in users] + user_choices.insert(0, ("", "Everyone")) + self.fields["user"].choices = user_choices + + for param in search.params: + # set initial values for modified params + value = search.__dict__.get(param) + if isinstance(value, models.Model): + self.fields[param].initial = value.id + else: + self.fields[param].initial = value + + # Mark non-editable modified fields as hidden. That way, templates + # rendering a form can just loop over hidden_fields to ensure that + # all necessary search options are kept when submitting the form. + if search.is_modified(param) and param not in editable_fields: + self.fields[param].widget = forms.HiddenInput() + + +class UserProfileForm(forms.ModelForm): + class Meta: + model = UserProfile + fields = [ + "theme", + "bookmark_date_display", + "bookmark_description_display", + "bookmark_description_max_lines", + "bookmark_link_target", + "web_archive_integration", + "tag_search", + "tag_grouping", + "enable_sharing", + "enable_public_sharing", + "enable_favicons", + "enable_preview_images", + "enable_automatic_html_snapshots", + "display_url", + "display_view_bookmark_action", + "display_edit_bookmark_action", + "display_archive_bookmark_action", + "display_remove_bookmark_action", + "permanent_notes", + "default_mark_unread", + "default_mark_shared", + "custom_css", + "auto_tagging_rules", + "items_per_page", + "sticky_pagination", + "collapse_side_panel", + "hide_bundles", + "legacy_search", + ] + widgets = { + "theme": FormSelect, + "bookmark_date_display": FormSelect, + "bookmark_description_display": FormSelect, + "bookmark_description_max_lines": FormNumberInput, + "bookmark_link_target": FormSelect, + "web_archive_integration": FormSelect, + "tag_search": FormSelect, + "tag_grouping": FormSelect, + "auto_tagging_rules": FormTextarea, + "custom_css": FormTextarea, + "items_per_page": FormNumberInput, + "display_url": FormCheckbox, + "permanent_notes": FormCheckbox, + "display_view_bookmark_action": FormCheckbox, + "display_edit_bookmark_action": FormCheckbox, + "display_archive_bookmark_action": FormCheckbox, + "display_remove_bookmark_action": FormCheckbox, + "sticky_pagination": FormCheckbox, + "collapse_side_panel": FormCheckbox, + "hide_bundles": FormCheckbox, + "legacy_search": FormCheckbox, + "enable_favicons": FormCheckbox, + "enable_preview_images": FormCheckbox, + "enable_sharing": FormCheckbox, + "enable_public_sharing": FormCheckbox, + "enable_automatic_html_snapshots": FormCheckbox, + "default_mark_unread": FormCheckbox, + "default_mark_shared": FormCheckbox, + } + + +class GlobalSettingsForm(forms.ModelForm): + class Meta: + model = GlobalSettings + fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"] + widgets = { + "landing_page": FormSelect, + "guest_profile_user": FormSelect, + "enable_link_prefetch": FormCheckbox, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["guest_profile_user"].empty_label = "Standard profile" diff --git a/bookmarks/frontend/components/tag-autocomplete.js b/bookmarks/frontend/components/tag-autocomplete.js index a167587..4d9d6fa 100644 --- a/bookmarks/frontend/components/tag-autocomplete.js +++ b/bookmarks/frontend/components/tag-autocomplete.js @@ -9,6 +9,7 @@ export class TagAutocomplete extends TurboLitElement { inputId: { type: String, attribute: "input-id" }, inputName: { type: String, attribute: "input-name" }, inputValue: { type: String, attribute: "input-value" }, + inputClass: { type: String, attribute: "input-class" }, inputPlaceholder: { type: String, attribute: "input-placeholder" }, inputAriaDescribedBy: { type: String, attribute: "input-aria-describedby" }, variant: { type: String }, @@ -160,7 +161,7 @@ export class TagAutocomplete extends TurboLitElement { name="${this.inputName || nothing}" .value="${this.inputValue || ""}" placeholder="${this.inputPlaceholder || " "}" - class="form-input" + class="form-input ${this.inputClass || ""}" type="text" autocomplete="off" autocapitalize="off" diff --git a/bookmarks/frontend/utils/element.js b/bookmarks/frontend/utils/element.js index e2bcb72..94a1104 100644 --- a/bookmarks/frontend/utils/element.js +++ b/bookmarks/frontend/utils/element.js @@ -42,10 +42,10 @@ document.addEventListener("turbo:render", () => { }); document.addEventListener("turbo:before-morph-element", (event) => { - if (event.target instanceof TurboLitElement) { - // Prevent Turbo from morphing Lit elements, which would remove rendered - // contents. For now this means that any Lit element / widget can not be - // updated from the server when using morphing. + const parent = event.target?.parentElement; + if (parent instanceof TurboLitElement) { + // Prevent Turbo from morphing Lit elements contents, which would remove + // elements rendered on the client side. event.preventDefault(); } }); diff --git a/bookmarks/models.py b/bookmarks/models.py index 9dd93de..745806b 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -3,7 +3,6 @@ import hashlib import logging import os -from django import forms from django.conf import settings from django.contrib.auth.models import User from django.core.validators import MinValueValidator @@ -195,12 +194,6 @@ class BookmarkBundle(models.Model): return self.name -class BookmarkBundleForm(forms.ModelForm): - class Meta: - model = BookmarkBundle - fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"] - - class BookmarkSearch: SORT_ADDED_ASC = "added_asc" SORT_ADDED_DESC = "added_desc" @@ -323,64 +316,6 @@ class BookmarkSearch: ) -class BookmarkSearchForm(forms.Form): - SORT_CHOICES = [ - (BookmarkSearch.SORT_ADDED_ASC, "Added ↑"), - (BookmarkSearch.SORT_ADDED_DESC, "Added ↓"), - (BookmarkSearch.SORT_TITLE_ASC, "Title ↑"), - (BookmarkSearch.SORT_TITLE_DESC, "Title ↓"), - ] - FILTER_SHARED_CHOICES = [ - (BookmarkSearch.FILTER_SHARED_OFF, "Off"), - (BookmarkSearch.FILTER_SHARED_SHARED, "Shared"), - (BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"), - ] - FILTER_UNREAD_CHOICES = [ - (BookmarkSearch.FILTER_UNREAD_OFF, "Off"), - (BookmarkSearch.FILTER_UNREAD_YES, "Unread"), - (BookmarkSearch.FILTER_UNREAD_NO, "Read"), - ] - - q = forms.CharField() - user = forms.ChoiceField(required=False) - bundle = forms.CharField(required=False) - sort = forms.ChoiceField(choices=SORT_CHOICES) - shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect) - unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect) - modified_since = forms.CharField(required=False) - added_since = forms.CharField(required=False) - - def __init__( - self, - search: BookmarkSearch, - editable_fields: list[str] = None, - users: list[User] = None, - ): - super().__init__() - editable_fields = editable_fields or [] - self.editable_fields = editable_fields - - # set choices for user field if users are provided - if users: - user_choices = [(user.username, user.username) for user in users] - user_choices.insert(0, ("", "Everyone")) - self.fields["user"].choices = user_choices - - for param in search.params: - # set initial values for modified params - value = search.__dict__.get(param) - if isinstance(value, models.Model): - self.fields[param].initial = value.id - else: - self.fields[param].initial = value - - # Mark non-editable modified fields as hidden. That way, templates - # rendering a form can just loop over hidden_fields to ensure that - # all necessary search options are kept when submitting the form. - if search.is_modified(param) and param not in editable_fields: - self.fields[param].widget = forms.HiddenInput() - - class UserProfile(models.Model): THEME_AUTO = "auto" THEME_LIGHT = "light" @@ -507,41 +442,6 @@ class UserProfile(models.Model): super().save(*args, **kwargs) -class UserProfileForm(forms.ModelForm): - class Meta: - model = UserProfile - fields = [ - "theme", - "bookmark_date_display", - "bookmark_description_display", - "bookmark_description_max_lines", - "bookmark_link_target", - "web_archive_integration", - "tag_search", - "tag_grouping", - "enable_sharing", - "enable_public_sharing", - "enable_favicons", - "enable_preview_images", - "enable_automatic_html_snapshots", - "display_url", - "display_view_bookmark_action", - "display_edit_bookmark_action", - "display_archive_bookmark_action", - "display_remove_bookmark_action", - "permanent_notes", - "default_mark_unread", - "default_mark_shared", - "custom_css", - "auto_tagging_rules", - "items_per_page", - "sticky_pagination", - "collapse_side_panel", - "hide_bundles", - "legacy_search", - ] - - @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: @@ -640,13 +540,3 @@ class GlobalSettings(models.Model): if not self.pk and GlobalSettings.objects.exists(): raise Exception("There is already one instance of GlobalSettings") return super().save(*args, **kwargs) - - -class GlobalSettingsForm(forms.ModelForm): - class Meta: - model = GlobalSettings - fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["guest_profile_user"].empty_label = "Standard profile" diff --git a/bookmarks/settings/base.py b/bookmarks/settings/base.py index 563337d..4640ab6 100644 --- a/bookmarks/settings/base.py +++ b/bookmarks/settings/base.py @@ -39,7 +39,6 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "widget_tweaks", "rest_framework", "rest_framework.authtoken", "huey.contrib.djhuey", diff --git a/bookmarks/styles/theme/autocomplete.css b/bookmarks/styles/theme/autocomplete.css index f8a74b0..df06279 100644 --- a/bookmarks/styles/theme/autocomplete.css +++ b/bookmarks/styles/theme/autocomplete.css @@ -31,6 +31,15 @@ outline: none; } } + + &:has(.is-error) { + background: var(--error-color-shade); + border-color: var(--error-color); + + &.is-focused { + outline-color: var(--error-color); + } + } } &.small { diff --git a/bookmarks/styles/theme/forms.css b/bookmarks/styles/theme/forms.css index 10567a0..af9a6e3 100644 --- a/bookmarks/styles/theme/forms.css +++ b/bookmarks/styles/theme/forms.css @@ -135,6 +135,10 @@ textarea.form-input { .is-error + & { color: var(--error-color); } + + &.is-error { + color: var(--error-color); + } } /* Form element: Select */ diff --git a/bookmarks/templates/bookmarks/form.html b/bookmarks/templates/bookmarks/form.html index 125fc40..22b14b6 100644 --- a/bookmarks/templates/bookmarks/form.html +++ b/bookmarks/templates/bookmarks/form.html @@ -1,38 +1,32 @@ -{% load widget_tweaks %} {% load static %} {% load shared %}
{% csrf_token %} - {{ form.auto_close|attr:"type:hidden" }} -
- + {{ form.auto_close }} +
+ {% formlabel form.url "URL" %}
- {{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }} + {% formfield form.url autofocus=True %}
- {% if form.url.errors %}
{{ form.url.errors }}
{% endif %} + {{ form.url.errors }}
This URL is already bookmarked. The form has been pre-filled with the existing bookmark, and saving the form will update the existing bookmark.
- - - -
+ {% formlabel form.tag_string "Tags" %} + {% formfield form.tag_string has_help=True %} + {% formhelp form.tag_string %} Enter any number of tags separated by space and without the hash (#). If a tag does not exist it will be automatically created. -
+ {% endformhelp %}
- {{ form.tag_string.errors }}
- + {% formlabel form.title "Title" %}
@@ -40,18 +34,16 @@
- {{ form.title|add_class:"form-input"|attr:"autocomplete:off" }} - {{ form.title.errors }} + {% formfield form.title %}
- + {% formlabel form.description "Description" %}
- {{ form.description|add_class:"form-input"|attr:"rows:3" }} - {{ form.description.errors }} + {% formfield form.description rows="3" %}
@@ -59,35 +51,28 @@ Notes - {{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }} -
Additional notes, supports Markdown.
+ {% formfield form.notes rows="8" has_help=True %} + {% formhelp form.notes %} + Additional notes, supports Markdown. + {% endformhelp %}
- {{ form.notes.errors }}
-
- {{ form.unread|form_field:"help" }} - - -
-
+ {% formfield form.unread label="Mark as unread" has_help=True %} + {% formhelp form.unread %} Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them. -
+ {% endformhelp %}
{% if request.user_profile.enable_sharing %}
-
- {{ form.shared|form_field:"help" }} - - -
-
+ {% formfield form.shared label="Share" has_help=True %} + {% formhelp form.shared %} {% if request.user_profile.enable_public_sharing %} Share this bookmark with other registered users and anonymous users. {% else %} Share this bookmark with other registered users. {% endif %} -
+ {% endformhelp %}
{% endif %}
diff --git a/bookmarks/templates/bookmarks/search.html b/bookmarks/templates/bookmarks/search.html index 08f4a13..ffee8c3 100644 --- a/bookmarks/templates/bookmarks/search.html +++ b/bookmarks/templates/bookmarks/search.html @@ -1,4 +1,4 @@ -{% load static widget_tweaks %} +{% load static shared %}
{% endif %} {% if 'shared' in preferences_form.editable_fields %} diff --git a/bookmarks/templates/bookmarks/user_section.html b/bookmarks/templates/bookmarks/user_section.html index ac16679..360e9e2 100644 --- a/bookmarks/templates/bookmarks/user_section.html +++ b/bookmarks/templates/bookmarks/user_section.html @@ -1,4 +1,4 @@ -{% load widget_tweaks %} +{% load shared %}

User

@@ -9,7 +9,7 @@ {% for hidden_field in user_list.form.hidden_fields %}{{ hidden_field }}{% endfor %}
- {% render_field user_list.form.user class+="form-select" data-submit-on-change="" %} + {% formfield user_list.form.user data_submit_on_change="" %} diff --git a/bookmarks/templates/bundles/edit.html b/bookmarks/templates/bundles/edit.html index c4b0df8..291e71e 100644 --- a/bookmarks/templates/bundles/edit.html +++ b/bookmarks/templates/bundles/edit.html @@ -1,5 +1,4 @@ {% extends 'shared/layout.html' %} -{% load widget_tweaks %} {% block head %} {% with page_title="Edit bundle - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} diff --git a/bookmarks/templates/bundles/form.html b/bookmarks/templates/bundles/form.html index b081817..7135407 100644 --- a/bookmarks/templates/bundles/form.html +++ b/bookmarks/templates/bundles/form.html @@ -1,38 +1,37 @@ -{% load widget_tweaks %} -
- - {{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }} - {% if form.name.errors %}
{{ form.name.errors }}
{% endif %} -
-
- - {{ form.search|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }} - {% if form.search.errors %}
{{ form.search.errors }}
{% endif %} -
All of these search terms must be present in a bookmark to match.
+{% load shared %} +
+ {% formlabel form.name "Name" %} + {% formfield form.name %} + {{ form.name.errors }}
- - - -
At least one of these tags must be present in a bookmark to match.
+ {% formlabel form.search "Search terms" %} + {% formfield form.search has_help=True %} + {{ form.search.errors }} + {% formhelp form.search %} + All of these search terms must be present in a bookmark to match. + {% endformhelp %}
- - - -
All of these tags must be present in a bookmark to match.
+ {% formlabel form.any_tags "Tags" %} + {% formfield form.any_tags has_help=True %} + {% formhelp form.any_tags %} + At least one of these tags must be present in a bookmark to match. + {% endformhelp %}
- - - -
None of these tags must be present in a bookmark to match.
+ {% formlabel form.all_tags "Required tags" %} + {% formfield form.all_tags has_help=True %} + {% formhelp form.all_tags %} + All of these tags must be present in a bookmark to match. + {% endformhelp %} +
+
+ {% formlabel form.excluded_tags "Excluded tags" %} + {% formfield form.excluded_tags has_help=True %} + {% formhelp form.excluded_tags %} + None of these tags must be present in a bookmark to match. + {% endformhelp %}
- {{ form.email|add_class:'form-input'|attr:"placeholder: " }} + {{ form.email }}
{{ form.errors.email }}
- {{ form.password1|add_class:'form-input'|attr:"placeholder: " }} + {{ form.password1 }}
{{ form.errors.password1 }}
- {{ form.password2|add_class:'form-input'|attr:"placeholder: " }} + {{ form.password2 }}
{{ form.errors.password2 }}

diff --git a/bookmarks/templates/registration/login.html b/bookmarks/templates/registration/login.html index ae38f47..16bbcb0 100644 --- a/bookmarks/templates/registration/login.html +++ b/bookmarks/templates/registration/login.html @@ -1,5 +1,5 @@ {% extends 'shared/layout.html' %} -{% load widget_tweaks %} +{% load shared %} {% block head %} {% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} @@ -11,17 +11,15 @@ {% csrf_token %} {% if form.errors %} -
-

Your username and password didn't match. Please try again.

-
+

Your username and password didn't match. Please try again.

{% endif %}
- - {{ form.username|add_class:'form-input'|attr:'placeholder: ' }} + {% formlabel form.username 'Username' %} + {% formfield form.username class='form-input' %}
- - {{ form.password|add_class:'form-input'|attr:'placeholder: ' }} + {% formlabel form.password 'Password' %} + {% formfield form.password class='form-input' %}

diff --git a/bookmarks/templates/registration/password_change_done.html b/bookmarks/templates/registration/password_change_done.html index f12264e..b566d23 100644 --- a/bookmarks/templates/registration/password_change_done.html +++ b/bookmarks/templates/registration/password_change_done.html @@ -1,5 +1,4 @@ {% extends 'shared/layout.html' %} -{% load widget_tweaks %} {% block head %} {% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} diff --git a/bookmarks/templates/registration/password_change_form.html b/bookmarks/templates/registration/password_change_form.html index 9a3b42a..2b37d48 100644 --- a/bookmarks/templates/registration/password_change_form.html +++ b/bookmarks/templates/registration/password_change_form.html @@ -1,5 +1,5 @@ {% extends 'shared/layout.html' %} -{% load widget_tweaks %} +{% load shared %} {% block head %} {% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %} {% endblock %} @@ -10,20 +10,20 @@
{% csrf_token %} -
- - {{ form.old_password|add_class:'form-input'|attr:"placeholder: " }} - {% if form.old_password.errors %}
{{ form.old_password.errors }}
{% endif %} +
+ {% formlabel form.old_password 'Old password' %} + {% formfield form.old_password class='form-input' %} + {{ form.old_password.errors }}
-
- - {{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }} - {% if form.new_password1.errors %}
{{ form.new_password1.errors }}
{% endif %} +
+ {% formlabel form.new_password1 'New password' %} + {% formfield form.new_password1 class='form-input' %} + {{ form.new_password1.errors }}
-
- - {{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }} - {% if form.new_password2.errors %}
{{ form.new_password2.errors }}
{% endif %} +
+ {% formlabel form.new_password2 'Confirm new password' %} + {% formfield form.new_password2 class='form-input' %} + {{ form.new_password2.errors }}

{% csrf_token %}
- - {{ form.theme|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.theme "Theme" %} + {% formfield form.theme has_help=True class="width-25 width-sm-100" %} + {% formhelp form.theme %} Whether to use a light or dark theme, or automatically adjust the theme based on your system's settings. -
+ {% endformhelp %}
- - {{ form.bookmark_date_display|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.bookmark_date_display "Bookmark date format" %} + {% formfield form.bookmark_date_display has_help=True class="width-25 width-sm-100" %} + {% formhelp form.bookmark_date_display %} Whether to show bookmark dates as relative (how long ago), or as absolute dates. Alternatively the date can be hidden. -
+ {% endformhelp %}
- - {{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.bookmark_description_display "Bookmark description" %} + {% formfield form.bookmark_description_display has_help=True class="width-25 width-sm-100" %} + {% formhelp form.bookmark_description_display %} Whether to show bookmark descriptions and tags in the same line, or as separate blocks. -
+ {% endformhelp %}
- - {{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }} -
Limits the number of lines that are displayed for the bookmark description.
+ {% formlabel form.bookmark_description_max_lines "Bookmark description max lines" %} + {% formfield form.bookmark_description_max_lines has_help=True class="width-25 width-sm-100" %} + {% formhelp form.bookmark_description_max_lines %} + Limits the number of lines that are displayed for the bookmark description. + {% endformhelp %}
- -
When enabled, this setting displays the bookmark URL below the title.
+ {% formfield form.display_url label="Show bookmark URL" has_help=True %} + {% formhelp form.display_url %} + When enabled, this setting displays the bookmark URL below the title. + {% endformhelp %}
- -
+ {% formfield form.permanent_notes label="Show notes permanently" has_help=True %} + {% formhelp form.permanent_notes %} Whether to show bookmark notes permanently, without having to toggle them individually. Alternatively the keyboard shortcut e can be used to temporarily show all notes. -
+ {% endformhelp %}
- - - - - + Bookmark actions + {% formfield form.display_view_bookmark_action label="View" %} + {% formfield form.display_edit_bookmark_action label="Edit" %} + {% formfield form.display_archive_bookmark_action label="Archive" %} + {% formfield form.display_remove_bookmark_action label="Remove" %}
Which actions to display for each bookmark.
- - {{ form.bookmark_link_target|add_class:"form-select width-25 width-sm-100" }} -
Whether to open bookmarks a new page or in the same page.
-
-
- - {{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }} - {% if form.items_per_page.errors %} -
{{ form.items_per_page.errors }}
- {% else %} - {% endif %} -
The number of bookmarks to display per page.
+ {% formlabel form.bookmark_link_target "Open bookmarks in" %} + {% formfield form.bookmark_link_target has_help=True class="width-25 width-sm-100" %} + {% formhelp form.bookmark_link_target %} + Whether to open bookmarks a new page or in the same page. + {% endformhelp %}
- -
+ {% formlabel form.items_per_page "Items per page" %} + {% formfield form.items_per_page has_help=True class="width-25 width-sm-100" min="10" %} + {{ form.items_per_page.errors }} + {% formhelp form.items_per_page %} + The number of bookmarks to display per page. + {% endformhelp %} +
+
+ {% formfield form.sticky_pagination label="Sticky pagination" has_help=True %} + {% formhelp form.sticky_pagination %} When enabled, the pagination controls will stick to the bottom of the screen, so that they are always visible without having to scroll to the end of the page first. -
+ {% endformhelp %}
- -
+ {% formfield form.collapse_side_panel label="Collapse side panel" has_help=True %} + {% formhelp form.collapse_side_panel %} When enabled, the tags side panel will be collapsed by default to give more space to the bookmark list. Instead, the tags are shown in an expandable drawer. -
+ {% endformhelp %}
- -
Allows to hide the bundles in the side panel if you don't intend to use them.
+ {% formfield form.hide_bundles label="Hide bundles" has_help=True %} + {% formhelp form.hide_bundles %} + Allows to hide the bundles in the side panel if you don't intend to use them. + {% endformhelp %}
- - {{ form.tag_search|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.tag_search "Tag search" %} + {% formfield form.tag_search has_help=True class="width-25 width-sm-100" %} + {% formhelp form.tag_search %} In strict mode, tags must be prefixed with a hash character (#). In lax mode, tags can also be searched without the hash character. Note that tags without the hash character are indistinguishable from search terms, which means the search result will also include bookmarks where a search term matches otherwise. -
+ {% endformhelp %}
- -
+ {% formfield form.legacy_search label="Enable legacy search" has_help=True %} + {% formhelp form.legacy_search %} Since version 1.44.0, linkding has a new search engine that supports logical expressions (and, or, not). If you run into any issues with the new search, you can enable this option to temporarily switch back to the old search. Please report any issues you encounter with the new search on GitHub so they can be addressed. This option will be removed in a future version. -
+ {% endformhelp %}
- - {{ form.tag_grouping|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.tag_grouping "Tag grouping" %} + {% formfield form.tag_grouping has_help=True class="width-25 width-sm-100" %} + {% formhelp form.tag_grouping %} In alphabetical mode, tags will be grouped by the first letter. If disabled, tags will not be grouped. -
+ {% endformhelp %}
@@ -176,21 +139,18 @@ -
{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}
+
{% formfield form.auto_tagging_rules has_help=True class="monospace" rows="6" %}
-
+ {% formhelp form.auto_tagging_rules %} Automatically adds tags to bookmarks based on predefined rules. Each line is a single rule that maps a URL to one or more tags. For example:
youtube.com video
 reddit.com/r/Music music reddit
-
+ {% endformhelp %}
- -
+ {% formfield form.enable_favicons label="Enable Favicons" has_help=True %} + {% formhelp form.enable_favicons %} Automatically loads favicons for bookmarked websites and displays them next to each bookmark. Enabling this feature automatically downloads all missing favicons. By default, this feature uses a Google service to download favicons. @@ -198,95 +158,68 @@ reddit.com/r/Music music reddit options documentation on how to configure a custom favicon provider. Icons are downloaded in the background, and it may take a while for them to show up. -
+ {% endformhelp %} {% if request.user_profile.enable_favicons and enable_refresh_favicons %} {% endif %}
- -
+ {% formfield form.enable_preview_images label="Enable Preview Images" has_help=True %} + {% formhelp form.enable_preview_images %} Automatically loads preview images for bookmarked websites and displays them next to each bookmark. Enabling this feature automatically downloads all missing preview images. -
+ {% endformhelp %}
- - {{ form.web_archive_integration|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel form.web_archive_integration "Internet Archive integration" %} + {% formfield form.web_archive_integration has_help=True class="width-25 width-sm-100" %} + {% formhelp form.web_archive_integration %} Enabling this feature will automatically create snapshots of bookmarked websites on the Internet Archive Wayback Machine. This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in case it goes offline or its content is modified. Please consider donating to the Internet Archive if you make use of this feature. -
+ {% endformhelp %}
- -
+ {% formfield form.enable_sharing label="Enable bookmark sharing" has_help=True %} + {% formhelp form.enable_sharing %} Allows to share bookmarks with other users, and to view shared bookmarks. Disabling this feature will hide all previously shared bookmarks from other users. -
+ {% endformhelp %}
- -
+ {% formfield form.enable_public_sharing label="Enable public bookmark sharing" has_help=True %} + {% formhelp form.enable_public_sharing %} Makes shared bookmarks publicly accessible, without requiring a login. That means that anyone with a link to this instance can view shared bookmarks via the shared bookmarks page. -
+ {% endformhelp %}
{% if has_snapshot_support %}
- -
+ {% formfield form.enable_automatic_html_snapshots label="Automatically create HTML snapshots" has_help=True %} + {% formhelp form.enable_automatic_html_snapshots %} Automatically creates HTML snapshots when adding bookmarks. Alternatively, when disabled, snapshots can be created manually in the details view of a bookmark. -
+ {% endformhelp %}
{% endif %}
- -
+ {% formfield form.default_mark_unread label="Create bookmarks as unread by default" has_help=True %} + {% formhelp form.default_mark_unread %} Sets the default state for the "Mark as unread" option when creating a new bookmark. Setting this option will make all new bookmarks default to unread. This can be overridden when creating each new bookmark. -
+ {% endformhelp %}
- -
+ {% formfield form.default_mark_shared label="Create bookmarks as shared by default" has_help=True %} + {% formhelp form.default_mark_shared %} Sets the default state for the "Share" option when creating a new bookmark. Setting this option will make all new bookmarks default to shared. This can be overridden when creating each new bookmark. -
+ {% endformhelp %}
@@ -294,9 +227,11 @@ reddit.com/r/Music music reddit Custom CSS -
{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}
+
{% formfield form.custom_css has_help=True class="monospace" rows="6" %}
-
Allows to add custom CSS to the page.
+ {% formhelp form.custom_css %} + Allows to add custom CSS to the page. + {% endformhelp %}
data-turbo="false"> {% csrf_token %}
- - {{ global_settings_form.landing_page|add_class:"form-select width-25 width-sm-100" }} -
The page that unauthenticated users are redirected to when accessing the root URL.
+ {% formlabel global_settings_form.landing_page "Landing page" %} + {% formfield global_settings_form.landing_page has_help=True class="width-25 width-sm-100" %} + {% formhelp global_settings_form.landing_page %} + The page that unauthenticated users are redirected to when accessing the root URL. + {% endformhelp %}
- - {{ global_settings_form.guest_profile_user|add_class:"form-select width-25 width-sm-100" }} -
+ {% formlabel global_settings_form.guest_profile_user "Guest user profile" %} + {% formfield global_settings_form.guest_profile_user has_help=True class="width-25 width-sm-100" %} + {% formhelp global_settings_form.guest_profile_user %} The user profile to use for users that are not logged in. This will affect how publicly shared bookmarks are displayed regarding theme, bookmark list settings, etc. You can either use your own profile or create a dedicated user for this purpose. By default, a standard profile with fixed settings is used. -
+ {% endformhelp %}
- -
+ {% formfield global_settings_form.enable_link_prefetch label="Enable prefetching links on hover" has_help=True %} + {% formhelp global_settings_form.enable_link_prefetch %} Prefetches internal links when hovering over them. This can improve the perceived performance when navigating application, but also increases the load on the server as well as bandwidth usage. -
+ {% endformhelp %}
{% csrf_token %}
-
+
When importing bookmarks from a service that supports marking bookmarks as public or private (using the PRIVATE attribute), enabling this option will import all bookmarks that are marked as not private as shared bookmarks. diff --git a/bookmarks/templates/shared/error_list.html b/bookmarks/templates/shared/error_list.html index 0c58c25..15769f3 100644 --- a/bookmarks/templates/shared/error_list.html +++ b/bookmarks/templates/shared/error_list.html @@ -2,7 +2,7 @@ {# Force rendering validation errors in English language to align with the rest of the app #} {% language 'en-us' %} {% if errors %} -
    {% for error in errors %}
  • {{ error }}
  • {% endfor %}
diff --git a/bookmarks/templates/tags/form.html b/bookmarks/templates/tags/form.html index 7b10311..1a9a104 100644 --- a/bookmarks/templates/tags/form.html +++ b/bookmarks/templates/tags/form.html @@ -1,9 +1,9 @@ -{% load widget_tweaks %} -
- - {{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: "|attr:"autofocus" }} -
+{% load shared %} +
+ {% formlabel form.name "Name" %} + {% formfield form.name has_help=True autofocus=True %} + {% formhelp form.name %} Tag names are case-insensitive and cannot contain spaces (spaces will be replaced with hyphens). -
- {% if form.name.errors %}
{{ form.name.errors }}
{% endif %} + {% endformhelp %} + {{ form.name.errors }}
diff --git a/bookmarks/templates/tags/merge.html b/bookmarks/templates/tags/merge.html index 53ca84b..5869191 100644 --- a/bookmarks/templates/tags/merge.html +++ b/bookmarks/templates/tags/merge.html @@ -1,4 +1,4 @@ -{% load widget_tweaks %} +{% load shared %} The merged tags are deleted -
- - - -
+
+ {% formlabel form.target_tag "Target tag" %} + {% formfield form.target_tag has_help=True %} + {% formhelp form.target_tag %} Enter the name of the tag you want to keep. The tags entered below will be merged into this one. -
- {% if form.target_tag.errors %}
{{ form.target_tag.errors }}
{% endif %} + {% endformhelp %} + {{ form.target_tag.errors }}
-
- - - -
+
+ {% formlabel form.merge_tags "Tags to merge" %} + {% formfield form.merge_tags has_help=True %} + {% formhelp form.merge_tags %} Enter the names of tags to merge into the target tag, separated by spaces. These tags will be deleted after merging. -
- {% if form.merge_tags.errors %}
{{ form.merge_tags.errors }}
{% endif %} + {% endformhelp %} + {{ form.merge_tags.errors }}