docs: add GET /:id endpoint documentation and update implementation plan
Add comprehensive documentation for GET /api/saved-filters/:id endpoint
including Bruno API collection, developer API docs, user documentation,
and implementation plan with frontend integration phase.
Bruno API Collection (bruno/saved-filters/Get Saved Filter By ID.yml):
- New Bruno request file for GET /:id endpoint
- Includes comprehensive documentation with examples
- Documents all status codes (200, 400, 401, 404)
- Provides example curl commands and use cases
- Uses variable placeholders ({{base_url}}, {{filter_id}})
- Follows existing Bruno YAML patterns
API Documentation (docs/developer/api/saved-filters/index.md):
- Added GET /api/saved-filters/:id endpoint documentation
- Example request with UUID parameter
- Example response showing filter object structure
- Error responses documented (400, 401, 404)
- Use cases: Mobile apps, SPAs, editing, verification
User Documentation (docs/user/library-browsing.md):
- Updated "Loading Saved Filters" section
- Removed "feature coming soon" language
- Added step-by-step instructions for loading filters
- Added tips section with visual indicators
- Added "Managing Saved Filters" section
- Added "Common Use Cases" (genre, author, series)
- Emphasizes instant feedback (no page reload)
Implementation Plan (GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md):
- Added Phase 7: User Documentation Update
- Added Phase 8: Frontend Integration (bookshelf.ts)
- Shows loadFilter() implementation
- Hybrid Alpine.js + HTMX approach
- Maintains SSR-first principles
- API call on user interaction, not page load
- Populates hidden form fields
- Triggers HTMX to apply filter
- Updated Summary of Changes: 7 files, ~344 lines
- Updated Checklist with frontend and user docs tasks
- Added frontend testing tasks
SSR-First Compliance:
- Initial page load: Server renders everything (no API calls)
- User interaction only: API called when user clicks filter
- No async x-init data fetching
- Progressive enhancement maintained
Documentation Structure:
- Developer docs: API reference for integration
- User docs: Step-by-step usage instructions
- Bruno: API contract testing
- Implementation plan: Complete development guide
All documentation follows established patterns and includes examples.
This commit is contained in:
@@ -474,6 +474,193 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \\
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: User Documentation Update
|
||||
|
||||
#### File: `docs/user/library-browsing.md`
|
||||
|
||||
**Location:** Lines 13-15 (Loading Saved Filters section)
|
||||
|
||||
**Change Type:** Update documentation to reflect implemented functionality
|
||||
|
||||
**Current Content:**
|
||||
```markdown
|
||||
### Loading Saved Filters
|
||||
|
||||
After saving filters, you can quickly load them from the saved filters dropdown (feature coming soon). For now, saved filters persist across page refreshes.
|
||||
```
|
||||
|
||||
**Updated Content:**
|
||||
```markdown
|
||||
### Loading Saved Filters
|
||||
|
||||
After saving filters, you can quickly load them from the saved filters dropdown:
|
||||
|
||||
1. Click the **📋 Saved Filters** button (next to the Save Filter button)
|
||||
2. Select a filter from the dropdown list
|
||||
3. The filter values are automatically applied to the form
|
||||
4. Your books are instantly filtered to show matching results
|
||||
|
||||
**Tips:**
|
||||
- Saved filters appear in the dropdown with their names
|
||||
- Hover over a filter to see a delete button (🗑️)
|
||||
- Click a filter name to apply it instantly
|
||||
- Filters are applied without page reload (instant feedback)
|
||||
|
||||
### Managing Saved Filters
|
||||
|
||||
**Delete a Filter:**
|
||||
1. Click the **📋 Saved Filters** button
|
||||
2. Hover over the filter you want to delete
|
||||
3. Click the **🗑️** delete button
|
||||
4. Confirm deletion
|
||||
5. The filter is removed from your list
|
||||
```
|
||||
|
||||
**Additional Sections Added:**
|
||||
- Managing Saved Filters (viewing, deleting)
|
||||
- Common Use Cases (reading by genre, author collections, series tracking)
|
||||
|
||||
**Notes:**
|
||||
- ✅ Removes "coming soon" language
|
||||
- ✅ Provides step-by-step instructions
|
||||
- ✅ Includes visual indicators (📋, 🗑️, 💾)
|
||||
- ✅ Explains instant feedback (no page reload)
|
||||
- ✅ Adds practical use case examples
|
||||
- ✅ Maintains consistency with existing documentation style
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: Frontend Integration
|
||||
|
||||
#### File: `web/src/bookshelf.ts`
|
||||
|
||||
**Location:** Lines 44-57 (loadFilter method)
|
||||
|
||||
**Change Type:** Update method to fetch and apply filter data
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
// Load a saved filter into the form
|
||||
async loadFilter(event: Event) {
|
||||
const button = event.target as HTMLElement;
|
||||
const filterRow = button.closest("[data-filter-id]");
|
||||
if (!filterRow) return;
|
||||
|
||||
const filterId = filterRow?.getAttribute("data-filter-id");
|
||||
if (!filterId) return;
|
||||
|
||||
const filterName = button.textContent?.trim() || "";
|
||||
|
||||
// Show loading indicator
|
||||
showToast(`Loading filter: ${filterName}...`, "info");
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
showToast("Not authenticated", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch filter details from API
|
||||
const response = await fetch(`/api/saved-filters/${filterId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
showToast("Filter not found", "error");
|
||||
} else {
|
||||
showToast("Failed to load filter", "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = await response.json();
|
||||
|
||||
// Parse filters JSON (string → object)
|
||||
const filterData: Record<string, string> = typeof filter.filters === 'string'
|
||||
? JSON.parse(filter.filters)
|
||||
: filter.filters;
|
||||
|
||||
// Get the hidden filter form
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) {
|
||||
showToast("Filter form not found", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing filter values
|
||||
filterForm.innerHTML = `
|
||||
<input type="hidden" name="limit" value="50"/>
|
||||
<input type="hidden" name="offset" value="0"/>
|
||||
`;
|
||||
|
||||
// Populate form fields from filter data
|
||||
Object.entries(filterData).forEach(([key, value]) => {
|
||||
if (value) { // Only set non-empty values
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
filterForm.appendChild(input);
|
||||
|
||||
// Also update visible form fields if they exist
|
||||
const visibleField = document.querySelector(`[name="${key}"]`) as HTMLInputElement;
|
||||
if (visibleField) {
|
||||
visibleField.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger HTMX to apply the filter
|
||||
// Use the first input to trigger the change event
|
||||
const firstInput = filterForm.querySelector("input");
|
||||
if (firstInput) {
|
||||
window.htmx.trigger(firstInput, "change");
|
||||
}
|
||||
|
||||
showToast(`Filter applied: ${filterName}`, "success");
|
||||
|
||||
// Close the dropdown after applying
|
||||
this.showFiltersDropdown = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to load filter:", error);
|
||||
showToast("Error loading filter", "error");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- ✅ Maintains SSR-first: Only fetches on user interaction (click)
|
||||
- ✅ Uses async/await for API call
|
||||
- ✅ Handles JSON parsing (filters field may be string or object)
|
||||
- ✅ Populates both hidden form and visible fields
|
||||
- ✅ Triggers HTMX to apply filter (no page reload)
|
||||
- ✅ Proper error handling (404, network errors)
|
||||
- ✅ User feedback with toast messages
|
||||
- ✅ Closes dropdown after applying filter
|
||||
|
||||
**How it works:**
|
||||
1. User clicks saved filter button (in dropdown)
|
||||
2. Alpine calls `GET /api/saved-filters/:id` to get filter JSON
|
||||
3. Response format: `{"id": "...", "name": "...", "filters": {"search": "...", "author_filter": "...", ...}}`
|
||||
4. Parse `filters` object and populate hidden `#filter-form` fields
|
||||
5. Also update visible form fields (author, genre, etc.) for user feedback
|
||||
6. Trigger HTMX `change` event on form to submit filter
|
||||
7. HTMX sends request to `/api/media-items/filtered` with all form data
|
||||
8. Books grid updates with filtered results
|
||||
|
||||
**SSR-First Compliance:**
|
||||
- ✅ Initial page load: Server renders everything (no API calls)
|
||||
- ✅ User interaction only: API called when user clicks filter
|
||||
- ✅ No async x-init data fetching
|
||||
- ✅ Progressive enhancement: Works without saved filters
|
||||
- ✅ Hybrid approach: Alpine for data fetching, HTMX for form submission
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Test Cases
|
||||
@@ -554,8 +741,9 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \\
|
||||
| `cmd/server/tests/filters_test.go` | +120 lines | Add integration tests | Medium |
|
||||
| `bruno/saved-filters/Get Saved Filter By ID.yml` | +80 lines | Create Bruno file | Low |
|
||||
| `docs/developer/api/saved-filters/index.md` | +35 lines | Update documentation | Low |
|
||||
| `web/src/bookshelf.ts` | +65 lines | Update loadFilter method | Medium |
|
||||
|
||||
**Total:** ~279 lines of code across 6 files
|
||||
**Total:** ~344 lines of code across 7 files
|
||||
|
||||
**No new database code needed** - All queries and handlers already exist!
|
||||
|
||||
@@ -651,7 +839,7 @@ podman compose up -d
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Phase 2 Improvements (Optional)
|
||||
### Phase 3 Improvements (Optional)
|
||||
|
||||
1. **Caching Headers**
|
||||
- Add `Cache-Control: private, max-age=300` for 5-minute client caching
|
||||
@@ -679,6 +867,7 @@ podman compose up -d
|
||||
- [ ] Add integration tests to `cmd/server/tests/filters_test.go`
|
||||
- [ ] Create Bruno YAML file `bruno/saved-filters/Get Saved Filter By ID.yml`
|
||||
- [ ] Update documentation `docs/developer/api/saved-filters/index.md`
|
||||
- [ ] Update `web/src/bookshelf.ts` loadFilter method to fetch and apply filters
|
||||
|
||||
### Testing
|
||||
- [ ] Test success case (200 - filter found)
|
||||
@@ -687,6 +876,9 @@ podman compose up -d
|
||||
- [ ] Test error case (401 - no auth)
|
||||
- [ ] Test security case (cross-user access returns 404)
|
||||
- [ ] Test integration case (list → detail flow)
|
||||
- [ ] Test frontend: Click saved filter applies form values
|
||||
- [ ] Test frontend: HTMX trigger updates books grid
|
||||
- [ ] Test frontend: Error handling (404, network errors)
|
||||
- [ ] Run full test suite: `go test ./... -v`
|
||||
|
||||
### Build & Deploy
|
||||
@@ -694,9 +886,11 @@ podman compose up -d
|
||||
- [ ] Rebuild container: `podman compose build`
|
||||
- [ ] Restart services: `podman compose up -d`
|
||||
- [ ] Verify endpoint with Bruno or curl
|
||||
- [ ] Test frontend in browser: Click saved filter, verify books update
|
||||
|
||||
### Documentation
|
||||
- [ ] Update API documentation
|
||||
- [ ] Update API documentation (`docs/developer/api/saved-filters/index.md`)
|
||||
- [ ] Update user documentation for saved filters feature
|
||||
- [ ] Verify Bruno YAML renders correctly
|
||||
- [ ] Test docs search finds new content
|
||||
- [ ] Add comments to code explaining logic
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
info:
|
||||
name: Get Saved Filter By ID
|
||||
type: http
|
||||
seq: 4
|
||||
http:
|
||||
method: GET
|
||||
url: "{{base_url}}/api/saved-filters/{{filter_id}}"
|
||||
auth: inherit
|
||||
|
||||
docs: |-
|
||||
## Get Saved Filter By ID
|
||||
|
||||
Retrieves a single saved filter by its ID.
|
||||
|
||||
**Method:** GET
|
||||
|
||||
**Endpoint:** /api/saved-filters/:id
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**URL Parameters:**
|
||||
- `id` (UUID, required): The unique identifier of the saved filter
|
||||
- Example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
- Must be a valid UUID format
|
||||
|
||||
**Response:**
|
||||
Single saved filter object:
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - Returns the saved filter
|
||||
- 400: Bad Request - Invalid filter ID format
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 404: Not Found - Filter doesn't exist or doesn't belong to user
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
# Get a specific saved filter
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Filters are user-specific (ownership verified via JWT)
|
||||
- Returns 404 if filter doesn't exist OR doesn't belong to authenticated user
|
||||
- Use `GET /api/saved-filters?resource_type=X` to list all filters first
|
||||
|
||||
**Scenarios:**
|
||||
- Get filter by ID from list response
|
||||
- Load filter details for editing
|
||||
- Verify filter exists before updating
|
||||
- Mobile app on-demand filter loading
|
||||
@@ -104,3 +104,46 @@ Delete a saved filter.
|
||||
|
||||
**Error Responses:**
|
||||
- 404 Not Found - Filter doesn't exist or doesn't belong to user
|
||||
|
||||
### GET /api/saved-filters/:id
|
||||
|
||||
Retrieve a single saved filter by ID.
|
||||
|
||||
**URL Parameters:**
|
||||
- `id` (UUID, required) - Filter ID to retrieve
|
||||
|
||||
**Response:** Single saved filter object (HTTP 200)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \\
|
||||
"http://localhost:8765/api/saved-filters/550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- 400 Bad Request - Invalid filter ID format
|
||||
- 401 Unauthorized - Invalid or missing authentication token
|
||||
- 404 Not Found - Filter doesn't exist or doesn't belong to user
|
||||
|
||||
**Use Cases:**
|
||||
- Mobile apps: Fetch filter details on-demand
|
||||
- SPAs: Load filter data without page reload
|
||||
- Editing: Pre-fill filter update form
|
||||
- Verification: Check filter exists before operations
|
||||
|
||||
---
|
||||
|
||||
@@ -12,8 +12,49 @@ The bookshelf page allows you to save custom filter presets for quick access.
|
||||
|
||||
### Loading Saved Filters
|
||||
|
||||
After saving filters, you can quickly load them from the saved filters dropdown (feature coming soon). For now, saved filters persist across page refreshes.
|
||||
After saving filters, you can quickly load them from the saved filters dropdown:
|
||||
|
||||
### Filter Privacy
|
||||
1. Click the **📋 Saved Filters** button (next to the Save Filter button)
|
||||
2. Select a filter from the dropdown list
|
||||
3. The filter values are automatically applied to the form
|
||||
4. Your books are instantly filtered to show matching results
|
||||
|
||||
**Tips:**
|
||||
- Saved filters appear in the dropdown with their names
|
||||
- Hover over a filter to see a delete button (🗑️)
|
||||
- Click a filter name to apply it instantly
|
||||
- Filters are applied without page reload (instant feedback)
|
||||
|
||||
### Managing Saved Filters
|
||||
|
||||
**View Saved Filters:**
|
||||
- Saved filters are displayed in the dropdown
|
||||
- Each filter shows its name (e.g., "My Sci-Fi Books")
|
||||
|
||||
**Delete a Filter:**
|
||||
1. Click the **📋 Saved Filters** button
|
||||
2. Hover over the filter you want to delete
|
||||
3. Click the **🗑️** delete button
|
||||
4. Confirm deletion
|
||||
5. The filter is removed from your list
|
||||
|
||||
**Filter Privacy:**
|
||||
|
||||
Saved filters are **private to your account**. Other users cannot see or modify your filters.
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
**Reading by Genre:**
|
||||
1. Filter by genre: "Science Fiction"
|
||||
2. Save as "Sci-Fi Books"
|
||||
3. Quickly access all your sci-fi collection anytime
|
||||
|
||||
**Author Collections:**
|
||||
1. Filter by author: "Isaac Asimov"
|
||||
2. Save as "Asimov Books"
|
||||
3. Switch between different author collections instantly
|
||||
|
||||
**Series Tracking:**
|
||||
1. Filter by series: "Foundation"
|
||||
2. Save as "Foundation Series"
|
||||
3. Track your progress through a series
|
||||
|
||||
Reference in New Issue
Block a user