eyecandy and delete
This commit is contained in:
@@ -640,3 +640,15 @@ Only print the `from` line if there is no title. Put the title directly behind t
|
|||||||
|
|
||||||
### Assistant outcome
|
### Assistant outcome
|
||||||
Updated Mastodon formatting to place the title directly after `From my #LinkLog:`, omit the `from:` line when no title is available, and place tags on a separate final line. Added no-title regression coverage. The focused tests passed and the complete Dockerized backend suite passed with 23 tests.
|
Updated Mastodon formatting to place the title directly after `From my #LinkLog:`, omit the `from:` line when no title is available, and place tags on a separate final line. Added no-title regression coverage. The focused tests passed and the complete Dockerized backend suite passed with 23 tests.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Do not show edit buttons on the home page. On `/<user>/`, show the edit button on the right and add a delete button.
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Added an owner-scoped `DELETE /api/links/{link_id}` endpoint. Feed edit/delete controls now render only on user-specific pages, never on the home feed, and are right-aligned in an entry action group. Added owner-only deletion regression coverage and updated the feed cache key. The complete Dockerized backend suite passed with 23 tests, and the running container is healthy.
|
||||||
|
|
||||||
|
### User
|
||||||
|
Put the buttons on the right-hand side of the link item, make them smaller, and keep the same color scheme.
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Changed the entry action group to float at the right edge of user-specific link items, allowing content to wrap around it. Reduced Edit and Delete button padding, radius, and font size while preserving Catppuccin surface and danger colors.
|
||||||
|
|||||||
@@ -117,6 +117,8 @@
|
|||||||
111. LINKLOG_PUBLIC_URL and TRAEFIK_HOST are the same and can be merged. (use LINKLOG_PUBLIC_URL), fix docker-compose to use said variable
|
111. LINKLOG_PUBLIC_URL and TRAEFIK_HOST are the same and can be merged. (use LINKLOG_PUBLIC_URL), fix docker-compose to use said variable
|
||||||
112. Format mastodon posts like From my #LinkLog: Title, optional comment, and from: URL
|
112. Format mastodon posts like From my #LinkLog: Title, optional comment, and from: URL
|
||||||
113. Only print the 'from' line if there is no title. Put the title directly behind the colon, and put all tags on the last line.
|
113. Only print the 'from' line if there is no title. Put the title directly behind the colon, and put all tags on the last line.
|
||||||
|
114. In the frontend do not show edit buttons on the home page even when a user is logged in. On the /<user>/ page show the edit button on the right of the entry. Also add a delete button.
|
||||||
|
115. Put the buttons on the right hand side of the link item, make the buttons smaller and same color scheme
|
||||||
|
|
||||||
## Future entries
|
## Future entries
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from fastapi import APIRouter, Header, HTTPException, status
|
|||||||
import logging
|
import logging
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.app.services.link_service import create_link, list_public_links, list_tags, update_link
|
from backend.app.services.link_service import create_link, delete_link, list_public_links, list_tags, update_link
|
||||||
from backend.app.services.plugin_manager import plugin_manager
|
from backend.app.services.plugin_manager import plugin_manager
|
||||||
from backend.app.services.token_service import validate_token
|
from backend.app.services.token_service import validate_token
|
||||||
|
|
||||||
@@ -73,6 +73,21 @@ def update_link_endpoint(
|
|||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete('/links/{link_id}')
|
||||||
|
def delete_link_endpoint(
|
||||||
|
link_id: str,
|
||||||
|
authorization: str | None = Header(default=None),
|
||||||
|
):
|
||||||
|
if not authorization or not authorization.startswith('Bearer '):
|
||||||
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
||||||
|
info = validate_token(authorization.replace('Bearer ', '', 1))
|
||||||
|
if info is None:
|
||||||
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
||||||
|
if not delete_link(link_id, info['user_id']):
|
||||||
|
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
||||||
|
return {'status': 'deleted', 'id': link_id}
|
||||||
|
|
||||||
|
|
||||||
@router.get('/links')
|
@router.get('/links')
|
||||||
def list_links():
|
def list_links():
|
||||||
return list_public_links()
|
return list_public_links()
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ def update_link(
|
|||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def delete_link(link_id: str, user_id: str) -> bool:
|
||||||
|
with get_connection() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
'DELETE FROM links WHERE id = ? AND user_id = ?',
|
||||||
|
(link_id, user_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def list_public_users():
|
def list_public_users():
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ def test_only_link_owner_can_edit_link():
|
|||||||
})
|
})
|
||||||
assert denied.status_code == 404
|
assert denied.status_code == 404
|
||||||
|
|
||||||
|
deleted = client.delete(f'/api/links/{link_id}', headers=owner_headers)
|
||||||
|
assert deleted.status_code == 200
|
||||||
|
assert client.delete(f'/api/links/{link_id}', headers=owner_headers).status_code == 404
|
||||||
|
assert client.delete(f'/api/links/{link_id}', headers=login_headers('bob')).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_logout_revokes_token_and_admin_can_list_plugins():
|
def test_logout_revokes_token_and_admin_can_list_plugins():
|
||||||
headers = login_headers()
|
headers = login_headers()
|
||||||
@@ -396,7 +401,8 @@ def test_public_and_admin_pages_render_html():
|
|||||||
assert '<a id="auth-username" class="user-name" href="/">' in admin_page
|
assert '<a id="auth-username" class="user-name" href="/">' in admin_page
|
||||||
assert 'admin.js?v=5' in admin_page
|
assert 'admin.js?v=5' in admin_page
|
||||||
feed_script = client.get('/static/feed.js?v=7').text
|
feed_script = client.get('/static/feed.js?v=7').text
|
||||||
assert 'if (item.is_owner)' in feed_script
|
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
||||||
|
assert 'deleteEntry(item, deleteButton)' in feed_script
|
||||||
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script
|
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' 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 'edit-tag-options' in feed_script
|
assert 'edit-tag-options' in feed_script
|
||||||
|
|||||||
+24
-2
@@ -117,13 +117,21 @@ function renderFeed(items, showIdentity = true) {
|
|||||||
meta.className = 'meta';
|
meta.className = 'meta';
|
||||||
meta.textContent = formatDate(item.created_at);
|
meta.textContent = formatDate(item.created_at);
|
||||||
|
|
||||||
if (item.is_owner) {
|
if (item.is_owner && !showIdentity) {
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'entry-actions';
|
||||||
const editButton = document.createElement('button');
|
const editButton = document.createElement('button');
|
||||||
editButton.type = 'button';
|
editButton.type = 'button';
|
||||||
editButton.className = 'edit-button';
|
editButton.className = 'edit-button';
|
||||||
editButton.textContent = 'Edit';
|
editButton.textContent = 'Edit';
|
||||||
editButton.addEventListener('click', () => showEditForm(article, item));
|
editButton.addEventListener('click', () => showEditForm(article, item));
|
||||||
article.appendChild(editButton);
|
const deleteButton = document.createElement('button');
|
||||||
|
deleteButton.type = 'button';
|
||||||
|
deleteButton.className = 'delete-button';
|
||||||
|
deleteButton.textContent = 'Delete';
|
||||||
|
deleteButton.addEventListener('click', () => deleteEntry(item, deleteButton));
|
||||||
|
actions.append(editButton, deleteButton);
|
||||||
|
article.appendChild(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showIdentity) {
|
if (showIdentity) {
|
||||||
@@ -144,6 +152,20 @@ function renderFeed(items, showIdentity = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteEntry(item, button) {
|
||||||
|
if (!window.confirm(`Delete this link?`)) return;
|
||||||
|
button.disabled = true;
|
||||||
|
const response = await fetch(`/api/links/${encodeURIComponent(item.id)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {Authorization: `Bearer ${accessToken}`},
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
await loadFeed();
|
||||||
|
} else {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showEditForm(article, item) {
|
function showEditForm(article, item) {
|
||||||
if (article.querySelector('.edit-form')) return;
|
if (article.querySelector('.edit-form')) return;
|
||||||
const form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
|
|||||||
@@ -420,11 +420,29 @@ button:disabled {
|
|||||||
|
|
||||||
.edit-button {
|
.edit-button {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-top: 14px;
|
padding: 5px 9px;
|
||||||
padding: 8px 12px;
|
border-radius: 6px;
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
border-color: var(--surface-2);
|
border-color: var(--surface-2);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-actions {
|
||||||
|
display: flex;
|
||||||
|
float: right;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 0 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 5px 9px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-color: rgba(243, 139, 168, 0.45);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-form {
|
.edit-form {
|
||||||
|
|||||||
@@ -79,6 +79,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/feed.js?v=7"></script>
|
<script src="/static/feed.js?v=8"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
Reference in New Issue
Block a user