remove/reorder targeted userRepos entries by array position only. An
external write to settings.json (another ALCOM instance, VCC, vrc-get
CLI, manual edit) between the frontend's last fetch and the call
could silently delete or reorder the wrong repo.
The frontend already has the expected id (part of TauriUserRepository),
so send it alongside the index and have the backend verify the repo at
that index still has the expected id before mutating. On mismatch,
reject with an `unrecoverable_str` error; the frontend's existing
onError + onSettled refetch rolls back optimistic state and shows the
on-disk truth.
- environment_remove_repository takes (index, expected_id)
- environment_reorder_repositories takes Vec<TauriUserRepositoryRef>;
all pairs verified up front, no partial reorder on mismatch
- verify_repo_at_index helper centralises the lookup + comparison
Auto-applied cargo fmt for the collect-chain split, then folded the
nested `if let` into a let-chain and replaced the manual `if let Some`
loop with `into_iter().flatten()`.
- vpm: drop unused id-based reorder_user_repos; reorder_user_repos_by_indices is the only caller
- CHANGELOG: reflect the actual public API surface (reorder_user_repos_by_indices, remove_repo_at_index)
- gui: rollback optimistic delete on error in RemoveRepositoryDialog, matching setHideRepository
- gui: demote per-reorder log from info to debug to reduce noise
Repos with duplicate (id, url) pairs had their per-row React keys
regenerated as `crypto.randomUUID()` on every refetch, causing scroll
reset and remount on the entire list. Stabilizing by `${id} ${url}`
key with an iteration-order counter would normally fix this, but for
duplicate-keyed rows the counter assignment depends on iteration
order: when two such repos swap positions, their slot keys swap, and
the lookup returns each other's UUID — React reconciles by tearing
both rows down (visible as a flicker on the row the dragged row
crossed over).
- Stabilize listId across refetch via listIdMapRef keyed on slot key
- Extract computeSlotKey helper for reuse
- Pin listIds to the new optimistic order in reorderMutation.onMutate
so the post-refetch augmentation lookup returns the same listIds
- Snapshot and restore the previous map in onError so a failed reorder
rolls back cleanly
Replaced sideEffects-based opacity hack (broken by React re-renders) with
a droppingListId state that keeps the source row hidden for the duration of
the drop animation via React's own style prop.
Removed the visualIndex estimate (rowIndex ± 1 based on transform.y) that
caused continuous className toggling during drag; rows now use their stable
rowIndex for stripe coloring, eliminating the flicker.
Two regressions introduced by the listId/UUID change:
- Scroll reset: crypto.randomUUID() in useMemo regenerated all listIds
on every re-fetch, causing React to unmount/remount all rows and lose
scroll position. Fixed by using String(r.index) instead — index is
stable across re-fetches for items that haven't moved, so React can
reconcile rows in place.
- Double-row artifact: on drop, isDragging becomes false before the
DragOverlay drop animation completes, making the real row visible at
the destination while the overlay is still animating there. Fixed by
using defaultDropAnimationSideEffects to force opacity:0 on the
original element for the duration of the drop animation.
Repositories with duplicate `id` values in settings.json caused the
frontend list to break: React keys were non-unique, dnd-kit's
SortableContext misbehaved, and the Map lookup silently discarded
duplicate entries.
- Add `index` field to `TauriUserRepository` (position in the
userRepos array, never persisted) so operations can target a
specific entry regardless of ID uniqueness
- Replace ID-based remove/reorder commands with index-based variants
(`remove_repo_at_index`, `reorder_user_repos_by_indices`) in both
vpm_settings and the Tauri command layer
- Augment each repo with a frontend-only `listId` (crypto.randomUUID)
used exclusively as React key and dnd-kit sortable item ID
When the vertical scrollbar is visible, its overlay covers the rightmost
10px of the table content. Add pe-2.5 to the content wrapper via CSS
so all ScrollableCardTable instances reserve space for the scrollbar.
- Move guiAnimation query and setHideRepository mutation from each
RepositoryRow to PageBody, passing them down as props. Because
useSortable subscribes all rows to DnD context, every pointermove
during drag re-rendered every row. With O(N) expensive hooks per row
this blocked the main thread for ~130ms per event (~7fps).
- Disable dnd-kit's built-in autoScroll (autoScroll={false}) and replace
it with a useDragAutoScroll hook. dnd-kit's AutoScroller caused jitter
due to wrong scroll-container detection in Radix UI ScrollArea and
double-smoothing from the viewport's scroll-behavior: smooth. The
custom hook targets the viewport directly via viewportRef and uses
behavior: instant to avoid CSS smooth-scroll interference.
- Suppress background-color transition on rows during active drag to
reduce paint operations when rows shift position.
- Replace orderedIds.includes() with a Set lookup in collisionDetection.
Track the hovered droppable via onDragOver and compute the overlay's
visual index so its background color updates in real-time as it moves
between rows, matching the same bg-secondary/30 / transition spec used
by the sortable rows.
Replace useDndContext() + arrayMove approach with transform.y sign
detection to compute visual row index. Previously all n rows subscribed
to the dnd context and re-rendered on every collision change, each
doing an O(n) arrayMove+indexOf -- O(n^2) total.
Now only rows whose transform changes re-render, and each computes its
visual index in O(1) from the sign of transform.y from useSortable.
Total cost is O(k) where k is the number of displaced rows (typically 1-2).
Also adds background-color to the inline transition string so the
200ms color animation works correctly alongside the transform animation,
and removes guiAnimation prop drilling through RepositoryTableBody.
Wrap checkbox in flex container to eliminate inline-block descender
space, which caused the button to align to the text baseline rather
than the vertical center of the cell.
- Remove redundant isDragging state; derive from activeId !== null
- Move TABLE_HEAD and DRAG_OVERLAY_MODIFIERS to module level to avoid
per-render allocations
- Remove unnecessary ?? true fallback on initialData: true query result
- Eliminate guiAnimation prop drilling through RepositoryTableBody;
RepositoryRow now reads directly from the shared React Query cache