15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-5 | 1 | execute | 1 |
|
true |
|
|
Purpose: Currently playlists are ordered by updated_at DESC from the database with no user control. Users need to organize playlists by different criteria.
Output: Sort dropdown in playlist header, client-side sorting with direction toggle, persisted preference via localStorage.
<execution_context> @/home/caleb/.config/Claude/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/Claude/get-shit-done/templates/summary.md </execution_context>
@frontend/src/components/playlist-view/playlist-view.ts — The main playlist view component (2798 lines). Sort dropdown goes in the header area. @frontend/src/components/track-list/track-list.ts — Has an existing sort toolbar pattern to replicate (lines 729-830 for CSS, 1599-1696 for render methods, 1358-1510 for sort logic). @frontend/src/store/playlist-store.ts — Playlist data store, provides `playlist.WithTracks[]`. @frontend/src/store/controllers/playlist-controller.ts — Controller bridging store to component. @backend/playlist/playlist.go — Go service; `Summary` struct (lines 43-47) needs `CreatedAt`/`UpdatedAt`. `GetAllPlaylistsWithTracks` (line 173) and `GetAllPlaylists` (line 147) construct Summary objects that need updating. @backend/database/sql/sqlcgen/models.go — Sqlc model: `Playlist` struct already has `CreatedAt`/`UpdatedAt` fields (lines 67-72). @frontend/wailsjs/go/models.ts — Auto-generated TypeScript models; `playlist.Summary` class (lines 305-318) will need `CreatedAt`/`UpdatedAt`. @backend/database/sql/schemas/playlists.sql — Schema: `created_at` and `updated_at` columns already exist. From backend/playlist/playlist.go: ```go type Summary struct { ID int64 `json:"ID"` Name string `json:"Name"` }type WithTracks struct {
Summary Summary json:"Summary"
Tracks []Track json:"Tracks"
}
From backend/database/sql/sqlcgen/models.go:
```go
type Playlist struct {
ID int64
Name string
CreatedAt time.Time
UpdatedAt time.Time
}
From frontend/wailsjs/go/models.ts:
export class Summary {
ID: number;
Name: string;
// CreatedAt and UpdatedAt NOT present yet — must be added
}
Sort toolbar CSS classes: .sort-toolbar, .sort-anchor, .sort-label, .sort-dir-btn, .sort-dropdown-panel, .active-sort, #sort-dropdown Sort state: sortField (string|null), sortDirection ('asc'|'desc'), sortDropdownOpen (boolean) localStorage keys pattern: 'track-list-sort-field', 'track-list-sort-direction'
Task 1: Add CreatedAt/UpdatedAt to playlist Summary struct and regenerate bindings backend/playlist/playlist.go backend/playlist/favorites.go frontend/wailsjs/go/models.ts 1. In `backend/playlist/playlist.go`, add `CreatedAt` and `UpdatedAt` fields to the `Summary` struct:type Summary struct {
ID int64 `json:"ID"`
Name string `json:"Name"`
CreatedAt string `json:"CreatedAt"`
UpdatedAt string `json:"UpdatedAt"`
}
Use string type (not time.Time) since Wails serializes time values as strings and the frontend only needs them for comparison sorting. Format as RFC3339 using p.CreatedAt.Format(time.RFC3339) and p.UpdatedAt.Format(time.RFC3339).
-
Update ALL locations where
Summary{}is constructed to include the new fields. Search the file forSummary{— there are ~16 occurrences across playlist.go and favorites.go. The main patterns:GetAllPlaylists(line 162): Has access top.CreatedAtandp.UpdatedAtfrom the sqlcPlayliststructGetAllPlaylistsWithTracks(line 239): Same —pissqlcgen.PlaylistCreatePlaylist/CreatePlaylistWithTracks/ImportSingle: After creating, the sqlcCreatePlaylistreturns*(RETURNING *) so the result hasCreatedAt/UpdatedAtRenamePlaylist(line 677): Doesn't have access to full row — use empty strings or re-query. Since this is an event payload (not display), empty strings are fine.GetOrCreateFavoritesPlaylistin favorites.go (line 141): HasplfromGetPlaylistwhich returns full row
For Summary constructions in event emission contexts (where CreatedAt/UpdatedAt aren't critical): populate with empty strings
""— the frontend ignores timestamps on event payloads. For Summary constructions returned to the frontend for display: populate with formatted time strings. -
Run
wails generateto regenerate the TypeScript bindings infrontend/wailsjs/go/models.ts. TheSummaryclass should now haveCreatedAt: stringandUpdatedAt: string. -
If
wails generateisn't available or fails, manually add the fields tofrontend/wailsjs/go/models.tsin theSummaryclass:- Add
CreatedAt: string;andUpdatedAt: string;as properties - Add them to the constructor:
this.CreatedAt = source["CreatedAt"];andthis.UpdatedAt = source["UpdatedAt"];cd backend && go build ./... && go vet ./... Summary struct includes CreatedAt/UpdatedAt strings, all construction sites updated, TypeScript bindings have the new fields, backend compiles cleanly.
- Add
1. Add sort state and constants:
Before the class definition, add:
type PlaylistSortField = 'name' | 'created' | 'modified' | 'tracks';
type SortDirection = 'asc' | 'desc';
const PLAYLIST_SORT_KEY = 'playlist-view-sort-field';
const PLAYLIST_SORT_DIR_KEY = 'playlist-view-sort-direction';
const SORT_OPTIONS: { id: PlaylistSortField; label: string }[] = [
{ id: 'modified', label: 'Recent' },
{ id: 'name', label: 'Name' },
{ id: 'created', label: 'Date Created' },
{ id: 'tracks', label: 'Track Count' },
];
Inside the class, add state properties:
@state() private sortField: PlaylistSortField = 'modified';
@state() private sortDirection: SortDirection = 'desc';
@state() private sortDropdownOpen = false;
@query('#sort-dropdown')
private sortDropdownPopup!: WaPopup;
2. Add sort CSS (inside the static styles array):
Copy the sort toolbar styles from track-list.ts (.sort-toolbar, .sort-anchor, .sort-anchor:hover, .sort-anchor .sort-label, .sort-dir-btn, .sort-dir-btn:hover, .sort-dropdown-panel, .sort-dropdown-panel wa-dropdown-item, .sort-dropdown-panel wa-dropdown-item:hover, .sort-dropdown-panel wa-dropdown-item.active-sort, #sort-dropdown). These are lines 731-830 of track-list.ts. Copy them verbatim — same CSS custom properties are used.
3. Add sort logic methods:
private restoreSortPreferences() {
try {
const field = localStorage.getItem(PLAYLIST_SORT_KEY);
if (field && SORT_OPTIONS.some(o => o.id === field)) {
this.sortField = field as PlaylistSortField;
}
const dir = localStorage.getItem(PLAYLIST_SORT_DIR_KEY);
if (dir === 'asc' || dir === 'desc') {
this.sortDirection = dir;
}
} catch { /* localStorage unavailable */ }
}
private saveSortPreferences() {
try {
localStorage.setItem(PLAYLIST_SORT_KEY, this.sortField);
localStorage.setItem(PLAYLIST_SORT_DIR_KEY, this.sortDirection);
} catch { /* localStorage unavailable */ }
}
private get sortedEntries(): PlaylistEntry[] {
const entries = this.filteredEntries;
const dir = this.sortDirection === 'asc' ? 1 : -1;
return [...entries].sort((a, b) => {
let cmp = 0;
switch (this.sortField) {
case 'name':
cmp = a.summary.Name.localeCompare(b.summary.Name);
break;
case 'created':
cmp = (a.summary.CreatedAt || '').localeCompare(b.summary.CreatedAt || '');
break;
case 'modified':
cmp = (a.summary.UpdatedAt || '').localeCompare(b.summary.UpdatedAt || '');
break;
case 'tracks':
cmp = a.tracks.length - b.tracks.length;
break;
}
return cmp * dir;
});
}
4. Add dropdown open/close/select methods (same pattern as track-list.ts):
toggleSortDropdown(),openSortDropdown(),closeSortDropdown()— same pattern as track-list.ts lines 1456-1490onSortDropdownSelect(field: PlaylistSortField)— setsthis.sortField = field, callssaveSortPreferences(),closeSortDropdown()toggleSortDirection()— flips direction, savessortDropdownCloseHandler— mousedown listener to close when clicking outside (same pattern as track-list.ts lines 1492-1510)
5. Register/unregister the mousedown close handler in connectedCallback and disconnectedCallback:
- In
connectedCallback(): adddocument.addEventListener('mousedown', this.sortDropdownCloseHandler); - Also call
this.restoreSortPreferences();inconnectedCallback() - In
disconnectedCallback(): adddocument.removeEventListener('mousedown', this.sortDropdownCloseHandler);
6. Add sort toolbar rendering as a private method renderSortToolbar():
private renderSortToolbar() {
const activeOption = SORT_OPTIONS.find(o => o.id === this.sortField);
const label = activeOption?.label ?? 'Recent';
const dirIcon = this.sortDirection === 'asc'
? 'arrow-up-short-wide'
: 'arrow-down-wide-short';
return html`
<div class="sort-toolbar">
<span>Sort:</span>
<button class="sort-anchor"
@click=${() => this.toggleSortDropdown()}
>
<span class="sort-label">${label}</span>
<wa-icon name="chevron-down"></wa-icon>
</button>
<button class="sort-dir-btn"
title="${this.sortDirection === 'asc' ? 'Ascending' : 'Descending'}"
@click=${() => this.toggleSortDirection()}
>
<wa-icon name=${dirIcon}></wa-icon>
</button>
</div>
${this.renderSortDropdownPopup()}
`;
}
private renderSortDropdownPopup() {
return html`
<wa-popup id="sort-dropdown"
placement="bottom-start" flip shift
.active=${this.sortDropdownOpen}
>
${this.sortDropdownOpen ? html`
<div class="sort-dropdown-panel">
${SORT_OPTIONS.map(opt => html`
<wa-dropdown-item
class=${this.sortField === opt.id ? 'active-sort' : ''}
@click=${() => this.onSortDropdownSelect(opt.id)}
>
${opt.label}
</wa-dropdown-item>
`)}
</div>
` : nothing}
</wa-popup>
`;
}
7. Wire sort toolbar into the render method:
In the render() method, insert the sort toolbar between the header </div> and the search indicator / create form. Specifically, after the importError block (after line 2124), add:
${this.renderSortToolbar()}
8. Replace filteredEntries with sortedEntries in the rendering path:
In renderPlaylistList(), change line 2459 from:
const visible = this.filteredEntries;
to:
const visible = this.sortedEntries;
Also update the originalIndex lookup on line 2488-2489. Since sortedEntries may reorder entries, this.entries.indexOf(entry) still works correctly since it finds the entry in the original this.entries array — the reference identity is preserved because sortedEntries spreads filteredEntries which filters this.entries. VERIFY this is the case. If filteredEntries creates new objects (it does NOT — it just filters), then indexOf will still work.
IMPORTANT: The direction button should ALWAYS be visible (unlike track-list which hides it when no sort is active), since playlist sort always has an active field (no "Default" option — "Recent" is the default). cd frontend && npx tsc --noEmit Playlist view has a sort toolbar below the header with four options (Recent, Name, Date Created, Track Count), a direction toggle button, dropdown opens/closes correctly, sort preference saved to localStorage, playlists reorder when sort changes. Default is "Recent" descending (matching current behavior).
1. `cd backend && go build ./... && go vet ./...` — backend compiles 2. `cd frontend && npx tsc --noEmit` — frontend type-checks 3. Manual: Open playlist view, verify sort dropdown appears, try each sort option, toggle direction, verify playlists reorder correctly 4. Manual: Switch away from playlist view and back — sort preference persists<success_criteria>
- Sort dropdown visible in playlist view header area
- Four sort options: Recent (default), Name, Date Created, Track Count
- Ascending/descending toggle works
- Playlists visually reorder when sort or direction changes
- Sort preference persists in localStorage across view switches
- Backend compiles, frontend type-checks
- Default sort (Recent, desc) matches the existing behavior (updated_at DESC from DB) </success_criteria>