Add filter for shared state (#531)

* Add shared filter to bookmark search model

* Add shared filter UI

* Implement shared filter

* Add API test

* Use radio buttons

* Rename shared parameter

* Improve radio button CSS
This commit is contained in:
Sascha Ißbrücker
2023-09-10 23:14:07 +03:00
committed by GitHub
parent b7ddee2d93
commit ffcc40b227
14 changed files with 168 additions and 24 deletions

View File

@@ -103,7 +103,7 @@
}
// Recent search suggestions
const search = searchHistory.getRecentSearches(value, 5).map(value => ({
const recentSearches = searchHistory.getRecentSearches(value, 5).map(value => ({
type: 'search',
index: nextIndex(),
label: value,
@@ -132,7 +132,7 @@
})
}
updateSuggestions(search, bookmarks, tagSuggestions)
updateSuggestions(recentSearches, bookmarks, tagSuggestions)
if (hasSuggestions()) {
open()
@@ -143,17 +143,17 @@
const debouncedLoadSuggestions = debounce(loadSuggestions)
function updateSuggestions(search, bookmarks, tagSuggestions) {
search = search || []
function updateSuggestions(recentSearches, bookmarks, tagSuggestions) {
recentSearches = recentSearches || []
bookmarks = bookmarks || []
tagSuggestions = tagSuggestions || []
suggestions = {
search,
recentSearches,
bookmarks,
tags: tagSuggestions,
total: [
...tagSuggestions,
...search,
...recentSearches,
...bookmarks,
]
}
@@ -215,10 +215,10 @@
</li>
{/each}
{#if suggestions.search.length > 0}
{#if suggestions.recentSearches.length > 0}
<li class="menu-item group-item">Recent Searches</li>
{/if}
{#each suggestions.search as suggestion}
{#each suggestions.recentSearches as suggestion}
<li class="menu-item" class:selected={selectedIndex === suggestion.index}>
<a href="#" on:mousedown|preventDefault={() => completeSuggestion(suggestion)}>
{suggestion.label}

View File

@@ -130,21 +130,28 @@ class BookmarkSearch:
SORT_TITLE_ASC = 'title_asc'
SORT_TITLE_DESC = 'title_desc'
params = ['q', 'user', 'sort']
FILTER_SHARED_OFF = ''
FILTER_SHARED_SHARED = 'shared'
FILTER_SHARED_UNSHARED = 'unshared'
params = ['q', 'user', 'sort', 'shared']
defaults = {
'q': '',
'user': '',
'sort': SORT_ADDED_DESC,
'shared': FILTER_SHARED_OFF,
}
def __init__(self,
q: str = defaults['q'],
query: str = defaults['q'], # alias for q
user: str = defaults['user'],
sort: str = defaults['sort']):
sort: str = defaults['sort'],
shared: str = defaults['shared']):
self.q = q or query
self.user = user
self.sort = sort
self.shared = shared
@property
def query(self):
@@ -180,10 +187,16 @@ class BookmarkSearchForm(forms.Form):
(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'),
]
q = forms.CharField()
user = forms.ChoiceField()
sort = forms.ChoiceField(choices=SORT_CHOICES)
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None):
super().__init__()

View File

@@ -69,6 +69,12 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
unread=True
)
# Shared filter
if search.shared == BookmarkSearch.FILTER_SHARED_SHARED:
query_set = query_set.filter(shared=True)
elif search.shared == BookmarkSearch.FILTER_SHARED_UNSHARED:
query_set = query_set.filter(shared=False)
# Sort by date added
if search.sort == BookmarkSearch.SORT_ADDED_ASC:
query_set = query_set.order_by('date_added')

View File

@@ -58,7 +58,7 @@
border-bottom-right-radius: 0;
}
.dropdown button {
.dropdown-toggle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@@ -71,7 +71,7 @@
.dropdown {
.menu {
padding: $unit-4;
min-width: 220px;
min-width: 250px;
}
&:focus-within {
@@ -85,6 +85,23 @@
display: flex;
justify-content: space-between;
}
.radio-group {
.form-label {
padding-bottom: 0;
}
.form-radio.form-inline {
margin: 0 $unit-2 0 0;
padding: 0;
display: inline-flex;
align-items: center;
column-gap: $unit-1;
}
.form-icon {
top: 0;
position: relative;
}
}
}
}

View File

@@ -63,6 +63,19 @@ a:visited:hover {
transition: none !important;
}
// Fix radio button sub-pixel size
.form-radio .form-icon {
width: 14px;
height: 14px;
border-width: 1px;
}
.form-radio input:checked + .form-icon::before {
top: 3px;
left: 3px;
transform: unset;
}
// Make code work with light and dark theme
code {
color: $gray-color-dark;

View File

@@ -44,6 +44,10 @@ a:focus, .btn:focus {
border-color: $dt-primary-button-color;
}
.form-radio input:checked + .form-icon::before {
background: $light-color;
}
// Pagination
.pagination .page-item.active a {
background: $dt-primary-button-color;

View File

@@ -27,6 +27,16 @@
<label for="{{ form.sort.id_for_label }}" class="form-label">Sort by</label>
{{ form.sort|add_class:"form-select select-sm" }}
</div>
<div class="form-group radio-group">
<div class="form-label">Shared filter</div>
{% for radio in form.shared %}
<label for="{{ radio.id_for_label }}" class="form-radio form-inline">
{{ radio.tag }}
<i class="form-icon"></i>
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
<div class="actions">
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
</div>
@@ -47,6 +57,7 @@
const search = {
q: '{{ search.query }}',
user: '{{ search.user }}',
shared: '{{ search.shared }}',
}
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
const input = document.querySelector('#search input[name="q"]')

View File

@@ -22,7 +22,7 @@ def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''):
tag_names = [tag.name for tag in tags]
tags_string = build_tag_string(tag_names, ' ')
form = BookmarkSearchForm(search, editable_fields=['q', 'sort'])
form = BookmarkSearchForm(search, editable_fields=['q', 'sort', 'shared'])
return {
'request': context['request'],
'search': search,

View File

@@ -12,13 +12,18 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
self.assertEqual(form['q'].initial, '')
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(form['user'].initial, '')
self.assertEqual(form['shared'].initial, '')
# with params
search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123')
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123',
shared=BookmarkSearch.FILTER_SHARED_SHARED)
form = BookmarkSearchForm(search)
self.assertEqual(form['q'].initial, 'search query')
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
self.assertEqual(form['user'].initial, 'user123')
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED)
def test_user_options(self):
users = [
@@ -43,16 +48,22 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
self.assertEqual(len(form.hidden_fields()), 0)
# some modified params
search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC)
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC)
form = BookmarkSearchForm(search)
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort']])
# all modified params
search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123')
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123',
shared=BookmarkSearch.FILTER_SHARED_SHARED)
form = BookmarkSearchForm(search)
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort'], form['user']])
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort'], form['user'], form['shared']])
# some modified params are editable fields
search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123')
search = BookmarkSearch(q='search query',
sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123')
form = BookmarkSearchForm(search, editable_fields=['q', 'user'])
self.assertCountEqual(form.hidden_fields(), [form['sort']])

View File

@@ -13,6 +13,7 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(search.q, '')
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(search.user, '')
self.assertEqual(search.shared, '')
# some params
mock_request.GET = {
@@ -24,18 +25,21 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(bookmark_search.q, 'search query')
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(bookmark_search.user, 'user123')
self.assertEqual(bookmark_search.shared, '')
# all params
mock_request.GET = {
'q': 'search query',
'user': 'user123',
'sort': BookmarkSearch.SORT_TITLE_ASC
'sort': BookmarkSearch.SORT_TITLE_ASC,
'shared': BookmarkSearch.FILTER_SHARED_SHARED,
}
search = BookmarkSearch.from_request(mock_request)
self.assertEqual(search.q, 'search query')
self.assertEqual(search.user, 'user123')
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
def test_modified_params(self):
# no params
@@ -44,7 +48,7 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(len(modified_params), 0)
# params are default values
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='')
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='')
modified_params = bookmark_search.modified_params
self.assertEqual(len(modified_params), 0)
@@ -54,6 +58,6 @@ class BookmarkSearchModelTest(TestCase):
self.assertCountEqual(modified_params, ['q', 'sort'])
# all modified params
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123')
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123', shared=BookmarkSearch.FILTER_SHARED_SHARED)
modified_params = bookmark_search.modified_params
self.assertCountEqual(modified_params, ['q', 'sort', 'user'])
self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared'])

View File

@@ -44,11 +44,13 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
self.assertNoHiddenInput(rendered_template, 'user')
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')
# With params
url = '/test?q=foo&user=john&sort=title_asc'
url = '/test?q=foo&user=john&sort=title_asc&shared=shared'
rendered_template = self.render_template(url)
self.assertHiddenInput(rendered_template, 'user', 'john')
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')

View File

@@ -70,6 +70,25 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], bookmarks)
def test_list_bookmarks_filter_shared(self):
self.authenticate()
unshared_bookmarks = self.setup_numbered_bookmarks(5)
shared_bookmarks = self.setup_numbered_bookmarks(5, shared=True)
# Filter off
response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks + shared_bookmarks)
# Filter shared
response = self.get(reverse('bookmarks:bookmark-list') + '?shared=shared',
expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
# Filter unshared
response = self.get(reverse('bookmarks:bookmark-list') + '?shared=unshared',
expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks)
def test_list_bookmarks_should_respect_sort(self):
self.authenticate()
bookmarks = self.setup_numbered_bookmarks(5)

View File

@@ -419,6 +419,25 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
self.assertCountEqual(list(query), unread_bookmarks)
def test_query_bookmarks_filter_shared(self):
unshared_bookmarks = self.setup_numbered_bookmarks(5)
shared_bookmarks = self.setup_numbered_bookmarks(5, shared=True)
# Filter is off
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_OFF)
query = queries.query_bookmarks(self.user, self.profile, search)
self.assertCountEqual(list(query), unshared_bookmarks + shared_bookmarks)
# Filter for shared
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_SHARED)
query = queries.query_bookmarks(self.user, self.profile, search)
self.assertCountEqual(list(query), shared_bookmarks)
# Filter for unshared
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_UNSHARED)
query = queries.query_bookmarks(self.user, self.profile, search)
self.assertCountEqual(list(query), unshared_bookmarks)
def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
self.setup_tag_search_data()
@@ -662,6 +681,29 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
BookmarkSearch(query=f'!untagged #{tag.name}'))
self.assertCountEqual(list(query), [])
def test_query_bookmark_tags_filter_shared(self):
unshared_bookmarks = self.setup_numbered_bookmarks(5, with_tags=True)
shared_bookmarks = self.setup_numbered_bookmarks(5, with_tags=True, shared=True)
unshared_tags = self.get_tags_from_bookmarks(unshared_bookmarks)
shared_tags = self.get_tags_from_bookmarks(shared_bookmarks)
all_tags = unshared_tags + shared_tags
# Filter is off
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_OFF)
query = queries.query_bookmark_tags(self.user, self.profile, search)
self.assertCountEqual(list(query), all_tags)
# Filter for shared
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_SHARED)
query = queries.query_bookmark_tags(self.user, self.profile, search)
self.assertCountEqual(list(query), shared_tags)
# Filter for unshared
search = BookmarkSearch(shared=BookmarkSearch.FILTER_SHARED_UNSHARED)
query = queries.query_bookmark_tags(self.user, self.profile, search)
self.assertCountEqual(list(query), unshared_tags)
def test_query_shared_bookmarks(self):
user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True)

View File

@@ -78,11 +78,13 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
self.assertNoHiddenInput(rendered_template, 'user')
self.assertNoHiddenInput(rendered_template, 'q')
self.assertNoHiddenInput(rendered_template, 'sort')
self.assertNoHiddenInput(rendered_template, 'shared')
# With params
url = '/test?q=foo&user=john&sort=title_asc'
url = '/test?q=foo&user=john&sort=title_asc&shared=shared'
rendered_template = self.render_template(url)
self.assertNoHiddenInput(rendered_template, 'user')
self.assertHiddenInput(rendered_template, 'q', 'foo')
self.assertHiddenInput(rendered_template, 'sort', 'title_asc')
self.assertHiddenInput(rendered_template, 'shared', 'shared')