feat(playlists): hybrid table over the drag list, and guard its destructive paths
The column sort and the filters are a VIEW; the playlist's stored order only changes when the user presses "Save this order", which still goes through the undoable path. Picking a sort used to write straight to the server, so merely looking at the list by title rewrote the playlist and marked a YouTube-linked mirror dirty. The table: - playlistColumns.tsx holds the column defs; the body renders them as table rows and fills the grip cell itself, since the handle needs the row's sortable listeners that a render closure can't reach. - The # column shows the STORED position, so a view-sort never renumbers the playlist under the reader. - Saving is withheld while a filter is active (it would drop the hidden items), and drag is disabled under any view, with the reason spelled out rather than silently doing nothing. - applyView/sameOrder live in lib/playlistView.ts with tests: the sort and the channel grouping compose, and getting that wrong produces a plausible order that the save button would then persist. - dnd-kit KeyboardSensor: the grip was focusable and called itself "reorder", but only a pointer could move it. The guards: - Delete is ONE dialog with every outcome. It was two chained confirms where the second one's Cancel/Escape/backdrop meant "delete here only" — dismissing the dialog deleted the playlist. ConfirmProvider gained useChoice() for it. - ConfirmProvider focuses Cancel when an offered action is destructive; the confirm button took autoFocus unconditionally, so a stray Enter on an open dialog ran "Delete user" or "Clear audit log". - Removing an item asks first, and rebases the undo history instead of throwing it away: useUndoable.rebase maps every snapshot, so the reorder history stays valid for the remaining items. - A failed detail query showed "Loading…" forever; it now says so and offers a retry. Rename and reorder failures are surfaced instead of swallowed (reorder was a bare .then(), which also produced an unhandled rejection). - Local reorder/remove no longer require YouTube write scope. - playlistName() replaces the watch-later localizer copied into three files.
This commit is contained in:
@@ -17,5 +17,6 @@
|
||||
"accessRequestsReview": "Review",
|
||||
"demoTitle": "Demo account",
|
||||
"demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time.",
|
||||
"backToTop": "Back to top"
|
||||
"backToTop": "Back to top",
|
||||
"retry": "Retry"
|
||||
}
|
||||
|
||||
@@ -42,16 +42,10 @@
|
||||
"pushDone": "“{{name}}” synced to YouTube ✓",
|
||||
"pushPartial": "Synced, but {{count}} item(s) were skipped.",
|
||||
"pushFailed": "Couldn't sync to YouTube.",
|
||||
"deleteOnYoutubeTitle": "Delete on YouTube too?",
|
||||
"deleteOnYoutubeMsg": "Also delete “{{name}}” from your YouTube account? Choose “Here only” to keep it on YouTube.",
|
||||
"deleteOnYoutubeConfirm": "Delete on YouTube",
|
||||
"deleteHereOnly": "Here only",
|
||||
"deleteYtFailed": "Couldn't delete on YouTube; removed here only.",
|
||||
"sortLabel": "Sort",
|
||||
"sortManual": "Custom order",
|
||||
"sortTitle": "Title",
|
||||
"sortDuration": "Duration",
|
||||
"sortChannel": "Channel",
|
||||
"dirAsc": "Ascending",
|
||||
"dirDesc": "Descending",
|
||||
"groupByChannel": "Group by channel",
|
||||
@@ -59,5 +53,25 @@
|
||||
"railSortName": "Name",
|
||||
"railSortCount": "Item count",
|
||||
"railSortDuration": "Total length",
|
||||
"dirtyFirst": "Unsynced first"
|
||||
"dirtyFirst": "Unsynced first",
|
||||
"colTitle": "Title",
|
||||
"colChannel": "Channel",
|
||||
"colDuration": "Length",
|
||||
"removeItem": "Remove",
|
||||
"removeItemOf": "Remove “{{title}}” from the playlist",
|
||||
"confirmRemove": "Remove “{{title}}” from this playlist?",
|
||||
"dragHandle": "Drag to reorder",
|
||||
"dragHandleOf": "Reorder “{{title}}”",
|
||||
"dragBlockedSort": "Reordering by hand is off while a column sort or grouping is applied.",
|
||||
"dragBlockedFilter": "Reordering by hand is off while a filter is applied.",
|
||||
"clearView": "Reset view",
|
||||
"saveOrder": "Save this order",
|
||||
"saveOrderHint": "Store the order you see as the playlist's own order (undoable).",
|
||||
"filteredCount": "{{count}} shown",
|
||||
"noneMatchFilter": "No videos match the filter.",
|
||||
"loadFailed": "Couldn't load this playlist.",
|
||||
"reorderFailed": "Couldn't save the new order.",
|
||||
"renameFailed": "Couldn't rename the playlist.",
|
||||
"deleteFailed": "Couldn't delete the playlist.",
|
||||
"confirmDeleteLinked": "Delete the playlist “{{name}}”? It is synced with YouTube — choose whether to delete it there as well. This can't be undone."
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
"accessRequestsReview": "Áttekintés",
|
||||
"demoTitle": "Demo fiók",
|
||||
"demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják.",
|
||||
"backToTop": "Vissza a tetejére"
|
||||
"backToTop": "Vissza a tetejére",
|
||||
"retry": "Újra"
|
||||
}
|
||||
|
||||
@@ -42,16 +42,10 @@
|
||||
"pushDone": "„{{name}}” szinkronizálva a YouTube-ra ✓",
|
||||
"pushPartial": "Szinkronizálva, de {{count}} elem kimaradt.",
|
||||
"pushFailed": "Nem sikerült a YouTube-ra szinkronizálás.",
|
||||
"deleteOnYoutubeTitle": "Törlöd a YouTube-on is?",
|
||||
"deleteOnYoutubeMsg": "Törlöd a(z) „{{name}}” listát a YouTube-fiókodból is? A „Csak itt” megtartja a YouTube-on.",
|
||||
"deleteOnYoutubeConfirm": "Törlés a YouTube-on",
|
||||
"deleteHereOnly": "Csak itt",
|
||||
"deleteYtFailed": "Nem sikerült törölni a YouTube-on; csak itt lett eltávolítva.",
|
||||
"sortLabel": "Rendezés",
|
||||
"sortManual": "Egyéni sorrend",
|
||||
"sortTitle": "Cím",
|
||||
"sortDuration": "Hossz",
|
||||
"sortChannel": "Csatorna",
|
||||
"dirAsc": "Növekvő",
|
||||
"dirDesc": "Csökkenő",
|
||||
"groupByChannel": "Csoportosítás csatorna szerint",
|
||||
@@ -59,5 +53,25 @@
|
||||
"railSortName": "Név",
|
||||
"railSortCount": "Elemszám",
|
||||
"railSortDuration": "Összhossz",
|
||||
"dirtyFirst": "Nem szinkronizáltak elöl"
|
||||
"dirtyFirst": "Nem szinkronizáltak elöl",
|
||||
"colTitle": "Cím",
|
||||
"colChannel": "Csatorna",
|
||||
"colDuration": "Hossz",
|
||||
"removeItem": "Eltávolítás",
|
||||
"removeItemOf": "„{{title}}” eltávolítása a listáról",
|
||||
"confirmRemove": "Eltávolítod a(z) „{{title}}” videót erről a listáról?",
|
||||
"dragHandle": "Húzd az átrendezéshez",
|
||||
"dragHandleOf": "„{{title}}” átrendezése",
|
||||
"dragBlockedSort": "A kézi átrendezés ki van kapcsolva, amíg oszloprendezés vagy csoportosítás van érvényben.",
|
||||
"dragBlockedFilter": "A kézi átrendezés ki van kapcsolva, amíg szűrő van érvényben.",
|
||||
"clearView": "Nézet visszaállítása",
|
||||
"saveOrder": "Sorrend mentése",
|
||||
"saveOrderHint": "A látott sorrend legyen a lista saját sorrendje (visszavonható).",
|
||||
"filteredCount": "{{count}} látszik",
|
||||
"noneMatchFilter": "Egy videó sem felel meg a szűrőnek.",
|
||||
"loadFailed": "Nem sikerült betölteni ezt a listát.",
|
||||
"reorderFailed": "Nem sikerült elmenteni az új sorrendet.",
|
||||
"renameFailed": "Nem sikerült átnevezni a listát.",
|
||||
"deleteFailed": "Nem sikerült törölni a listát.",
|
||||
"confirmDeleteLinked": "Törlöd a(z) „{{name}}” listát? A YouTube-bal szinkronban van — válaszd ki, hogy ott is törlődjön-e. Nem vonható vissza."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user