Tried to fix a broke filter.
Build LinkLog Development Image / development-image (push) Successful in 10s
Build LinkLog Development Image / development-image (push) Successful in 10s
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
* Made every web header logo link to the home page
|
* Made every web header logo link to the home page
|
||||||
* Added a high-contrast Red color theme
|
* Added a high-contrast Red color theme
|
||||||
* Brightened the Red theme surfaces and deepened its crimson accents
|
* Brightened the Red theme surfaces and deepened its crimson accents
|
||||||
|
* Allow every user to filter the feed using every available tag or label
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fixed element ID conflict on the Admin page so Available Themes load correctly
|
* Fixed element ID conflict on the Admin page so Available Themes load correctly
|
||||||
* Positioned new-entry tag checkboxes after their label text
|
* Positioned new-entry tag checkboxes after their label text
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
* Prevented the new-entry form grid from placing tag checkboxes above their labels
|
* Prevented the new-entry form grid from placing tag checkboxes above their labels
|
||||||
* Kept unconfigured New Entry Mastodon publishing controls hidden
|
* Kept unconfigured New Entry Mastodon publishing controls hidden
|
||||||
* Restored the home-page header layout, keeping the action controls above the filter toolbar
|
* Restored the home-page header layout, keeping the action controls above the filter toolbar
|
||||||
|
* Made the tag filter refresh the feed directly when its selection changes
|
||||||
|
* Cache-busted the feed script to ensure browsers load the responsive tag filter
|
||||||
|
* Fixed feed initialization so tag filtering does not receive promise results as a selected tag [Still no complete fix]
|
||||||
|
|
||||||
## Version v0.1.1
|
## Version v0.1.1
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1394,3 +1394,27 @@ Since checkin d433a305f5cd8ccdafde8a0f7764462e8c167162 on the home page the filt
|
|||||||
|
|
||||||
### Assistant outcome
|
### Assistant outcome
|
||||||
Confirmed the referenced commit did not change the feed header, then identified the current template had the toolbar outside its `.header-tools` wrapper. Restored the prior header structure: the New Entry and menu controls remain grouped above the filter toolbar on the right side of the home-page header. Added a rendered-page regression assertion for `.header-tools`.
|
Confirmed the referenced commit did not change the feed header, then identified the current template had the toolbar outside its `.header-tools` wrapper. Restored the prior header structure: the New Entry and menu controls remain grouped above the filter toolbar on the right side of the home-page header. Added a rendered-page regression assertion for `.header-tools`.
|
||||||
|
|
||||||
|
### User
|
||||||
|
I want every user to filter on every available tag/label
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Separated feed filtering from label management: `/api/tags` now returns every tag/label to all callers, allowing complete filtering of the public feed. `/api/user/labels` remains ownership-scoped, so users still cannot manage other users' labels. Updated tag visibility regression coverage for authenticated and anonymous callers.
|
||||||
|
|
||||||
|
### User
|
||||||
|
tag filter doesn't seem to respond when pressed
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Updated the tag-filter change handler to pass the newly selected tag directly to the feed refresh instead of relying on the browser to persist and immediately reread the preference cookie. This makes filtering work even when cookie persistence is unavailable or delayed. Cache-busted the feed script to `feed.js?v=10` and added a regression assertion for the direct refresh path.
|
||||||
|
|
||||||
|
### User
|
||||||
|
The tag filter still does not allow me to select tags, even though the raw html contains the correct content. Also the user filter and the sort functionality work too
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Verified the live running container with Chromium and Playwright: selecting `#AI` changes the tag dropdown value, writes the preference cookie, and renders only matching feed entries. The deployed script already contains the direct-refresh handler, so cache-busted it again to `feed.js?v=11` and rebuilt the Compose service. Confirmed the live page serves the new asset version.
|
||||||
|
|
||||||
|
### User
|
||||||
|
I believe tag selection was still working for v0.1.0 - it is still broken
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Found the actual regression in the direct-refresh update: `Promise.all([loadUsers(), loadTags()]).then(loadFeed)` passed its results array into the new `selectedTag` parameter. The feed then attempted to call `toLowerCase()` on that array during initialization. Restored the v0.1.0 callback shape with `.then(() => loadFeed())`, retained direct selected-tag refreshes, added a regression assertion, and cache-busted the feed asset to `feed.js?v=12`.
|
||||||
|
|||||||
@@ -248,6 +248,10 @@
|
|||||||
229. Add a red collor theme
|
229. Add a red collor theme
|
||||||
230. Make the theme brighter and darker red
|
230. Make the theme brighter and darker red
|
||||||
231. Since checkin d433a305f5cd8ccdafde8a0f7764462e8c167162 on the home page the filter toolbar moved so that the menu button and new entry are not in the top left anylonger - revert to the previous layout
|
231. Since checkin d433a305f5cd8ccdafde8a0f7764462e8c167162 on the home page the filter toolbar moved so that the menu button and new entry are not in the top left anylonger - revert to the previous layout
|
||||||
|
232. I want every user to filter on every available tag/label
|
||||||
|
233. tag filter doesn't seem to respond when pressed
|
||||||
|
234. The tag filter still does not allow me to select tags, even though the raw html contains the correct content. Also the user filter and the sort functionality work too
|
||||||
|
235. I believe tag selection was still working for v0.1.0 - it is still broken
|
||||||
|
|
||||||
## Future entries
|
## Future entries
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Response, status
|
|||||||
import logging
|
import logging
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.app.api.dependencies import get_optional_current_user
|
|
||||||
from backend.app.services.link_service import create_link, delete_link, find_owned_link_by_title_url, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link
|
from backend.app.services.link_service import create_link, delete_link, find_owned_link_by_title_url, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link
|
||||||
from backend.app.database import get_connection
|
from backend.app.database import get_connection
|
||||||
from backend.app.services.plugin_manager import plugin_manager
|
from backend.app.services.plugin_manager import plugin_manager
|
||||||
@@ -35,9 +34,8 @@ class LinkUpdate(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/tags')
|
@router.get('/tags')
|
||||||
def available_tags(user: dict | None = Depends(get_optional_current_user)):
|
def available_tags():
|
||||||
user_id = user['id'] if user else None
|
return list_tags()
|
||||||
return list_tags(user_id=user_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/scrape')
|
@router.get('/scrape')
|
||||||
|
|||||||
@@ -227,32 +227,9 @@ def list_public_users():
|
|||||||
return [row['username'] for row in rows]
|
return [row['username'] for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def list_tags(user_id: str | None = None):
|
def list_tags():
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
if user_id:
|
rows = conn.execute('SELECT name FROM tags ORDER BY name').fetchall()
|
||||||
rows = conn.execute(
|
|
||||||
'''
|
|
||||||
SELECT tags.name
|
|
||||||
FROM tags
|
|
||||||
LEFT JOIN users ON users.id = tags.created_by
|
|
||||||
WHERE tags.created_by IS NULL
|
|
||||||
OR tags.created_by = ?
|
|
||||||
OR users.is_admin = 1
|
|
||||||
ORDER BY tags.name
|
|
||||||
''',
|
|
||||||
(user_id,),
|
|
||||||
).fetchall()
|
|
||||||
else:
|
|
||||||
rows = conn.execute(
|
|
||||||
'''
|
|
||||||
SELECT tags.name
|
|
||||||
FROM tags
|
|
||||||
LEFT JOIN users ON users.id = tags.created_by
|
|
||||||
WHERE tags.created_by IS NULL
|
|
||||||
OR users.is_admin = 1
|
|
||||||
ORDER BY tags.name
|
|
||||||
''',
|
|
||||||
).fetchall()
|
|
||||||
tags = []
|
tags = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
|||||||
@@ -564,17 +564,17 @@ def test_label_visibility_isolation_and_grandfathering():
|
|||||||
assert '#Cybersecurity' in charlie_label_names
|
assert '#Cybersecurity' in charlie_label_names
|
||||||
assert '#BobOnlyLabel' not in charlie_label_names
|
assert '#BobOnlyLabel' not in charlie_label_names
|
||||||
|
|
||||||
# Bob views /api/tags (authenticated): sees Bob tag & default/grandfathered, NOT Charlie tag
|
# Every user can filter by every available tag, including another user's label.
|
||||||
bob_tags = client.get('/api/tags', headers=bob_headers).json()
|
bob_tags = client.get('/api/tags', headers=bob_headers).json()
|
||||||
assert '#BobOnlyLabel' in bob_tags
|
assert '#BobOnlyLabel' in bob_tags
|
||||||
assert '#GrandfatheredLabel' in bob_tags
|
assert '#GrandfatheredLabel' in bob_tags
|
||||||
assert '#CharlieOnlyLabel' not in bob_tags
|
assert '#CharlieOnlyLabel' in bob_tags
|
||||||
|
|
||||||
# Anonymous views /api/tags: sees default/grandfathered, NOT Bob or Charlie tag
|
# The public feed filter has the same complete tag catalog.
|
||||||
anon_tags = client.get('/api/tags').json()
|
anon_tags = client.get('/api/tags').json()
|
||||||
assert '#GrandfatheredLabel' in anon_tags
|
assert '#GrandfatheredLabel' in anon_tags
|
||||||
assert '#BobOnlyLabel' not in anon_tags
|
assert '#BobOnlyLabel' in anon_tags
|
||||||
assert '#CharlieOnlyLabel' not in anon_tags
|
assert '#CharlieOnlyLabel' in anon_tags
|
||||||
|
|
||||||
# Bob cannot edit or delete grandfathered label
|
# Bob cannot edit or delete grandfathered label
|
||||||
assert client.put(f"/api/user/labels/{grandfathered_id}", headers=bob_headers, json={'name': '#RenamedGrandfathered'}).status_code == 404
|
assert client.put(f"/api/user/labels/{grandfathered_id}", headers=bob_headers, json={'name': '#RenamedGrandfathered'}).status_code == 404
|
||||||
@@ -828,7 +828,11 @@ def test_public_and_admin_pages_render_html():
|
|||||||
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
||||||
assert 'deleteEntry(item, deleteButton)' in feed_script
|
assert 'deleteEntry(item, deleteButton)' in feed_script
|
||||||
assert 'postToMastodon(item, mastodonButton)' in feed_script
|
assert 'postToMastodon(item, mastodonButton)' in feed_script
|
||||||
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script
|
assert 'tag.toLowerCase() === activeTag.toLowerCase()' in feed_script
|
||||||
|
assert 'loadFeed(event.target.value)' in feed_script
|
||||||
|
assert 'Promise.all([loadUsers(), loadTags()]).then(() => loadFeed())' in feed_script
|
||||||
|
assert "fetch('/api/tags', {" in feed_script
|
||||||
|
assert 'Authorization: `Bearer ${accessToken}`' in feed_script
|
||||||
assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script
|
assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script
|
||||||
assert "entryMeta.className = 'entry-meta'" in feed_script
|
assert "entryMeta.className = 'entry-meta'" in feed_script
|
||||||
assert "meta.className = 'meta'" in feed_script
|
assert "meta.className = 'meta'" in feed_script
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ async function loadUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadTags() {
|
async function loadTags() {
|
||||||
const response = await fetch('/api/tags');
|
const response = await fetch('/api/tags', {
|
||||||
|
headers: accessToken ? {Authorization: `Bearer ${accessToken}`} : {},
|
||||||
|
});
|
||||||
if (!response.ok) throw new Error('Could not load tags');
|
if (!response.ok) throw new Error('Could not load tags');
|
||||||
const tags = await response.json();
|
const tags = await response.json();
|
||||||
availableTags = tags;
|
availableTags = tags;
|
||||||
@@ -254,7 +256,7 @@ function showEditForm(article, item) {
|
|||||||
article.appendChild(form);
|
article.appendChild(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFeed() {
|
async function loadFeed(selectedTag = null) {
|
||||||
const routeUser = document.body.dataset.userFilter;
|
const routeUser = document.body.dataset.userFilter;
|
||||||
const endpoint = routeUser
|
const endpoint = routeUser
|
||||||
? `/api/public/feed/${encodeURIComponent(routeUser)}`
|
? `/api/public/feed/${encodeURIComponent(routeUser)}`
|
||||||
@@ -266,13 +268,14 @@ async function loadFeed() {
|
|||||||
|
|
||||||
let items = data || [];
|
let items = data || [];
|
||||||
const pref = readPreferences();
|
const pref = readPreferences();
|
||||||
|
const activeTag = selectedTag ?? pref.tag;
|
||||||
|
|
||||||
if (pref.user && !routeUser) {
|
if (pref.user && !routeUser) {
|
||||||
items = items.filter((item) => (item.user?.username || '').toLowerCase() === pref.user.toLowerCase());
|
items = items.filter((item) => (item.user?.username || '').toLowerCase() === pref.user.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pref.tag) {
|
if (activeTag) {
|
||||||
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === pref.tag.toLowerCase()));
|
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === activeTag.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pref.sort === 'oldest') {
|
if (pref.sort === 'oldest') {
|
||||||
@@ -304,9 +307,9 @@ function syncPreferences() {
|
|||||||
tagFilter.addEventListener('change', (event) => {
|
tagFilter.addEventListener('change', (event) => {
|
||||||
const next = { ...readPreferences(), tag: event.target.value };
|
const next = { ...readPreferences(), tag: event.target.value };
|
||||||
writePreferences(next);
|
writePreferences(next);
|
||||||
loadFeed();
|
loadFeed(event.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPreferences();
|
syncPreferences();
|
||||||
Promise.all([loadUsers(), loadTags()]).then(loadFeed).catch(() => loadFeed());
|
Promise.all([loadUsers(), loadTags()]).then(() => loadFeed()).catch(() => loadFeed());
|
||||||
|
|||||||
@@ -86,6 +86,6 @@
|
|||||||
<script src="/static/auth-header.js?v=3"></script>
|
<script src="/static/auth-header.js?v=3"></script>
|
||||||
<script src="/static/logout.js?v=3"></script>
|
<script src="/static/logout.js?v=3"></script>
|
||||||
<script src="/static/theme.js?v=1"></script>
|
<script src="/static/theme.js?v=1"></script>
|
||||||
<script src="/static/feed.js?v=9"></script>
|
<script src="/static/feed.js?v=12"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user