Skip to content

Catalog to planner dnd - #113

Open
liug88 wants to merge 20 commits into
main-previewfrom
catalog-to-planner-dnd
Open

Catalog to planner dnd#113
liug88 wants to merge 20 commits into
main-previewfrom
catalog-to-planner-dnd

Conversation

@liug88

@liug88 liug88 commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Catalog to Planner Drag and Drop

Adds the ability to drag courses directly from the Catalog into a Planner semester block.

Changes

Drag source — CatalogCourse.tsx

  • Wired up useDraggable with type "catalog-course" so each catalog card is draggable
  • The original card stays in place and dims to 50% opacity while dragging, giving clear visual feedback without disrupting the list

Drag ghost — dnd.tsx + DragOverlay

  • Instead of the full catalog card flying across the screen, a compact planner-style card (dark blue, matching existing planner course cards) is shown as the drag ghost via DragOverlay
  • The dragged APICourse is captured in state on dragStart and cleared on dragEnd, which feeds static children to DragOverlay — this avoids relying on dnd-kit's reactive signal internals for rendering
  • dropAnimation={null} prevents the ghost from snapping back to the catalog on a successful drop

Drop target — SemesterBlock.tsx

  • Added "catalog-course" to each semester's accept list so catalog cards can land in semester blocks

Drop logic — dnd.tsx

  • Catalog-to-planner drops are handled in onDragEnd only (not onDragOver) because they create a new UserCourse entry rather than moving an existing one — processing it live on every hover would be premature
  • On drop, a fresh UUID is assigned so the same course can appear in multiple semesters independently

Accessibility

  • Added type="button" and aria-label to drag handles, the options button, the add-to-toolbox button, and the garbage bin button

liug88 added 2 commits March 31, 2026 16:27
Yeah, it can do it now but it looks a little awkward. Will fix in next commit.
Now, when a course is dragged from the course list, the blue copy is spawned and be dragged accordingly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds catalog-to-planner drag-and-drop support so users can drag a course card from the Catalog and drop it into a Planner semester, creating a new UserCourse entry and showing a compact planner-style drag ghost.

Changes:

  • Made catalog course cards draggable with DnD metadata (type: "catalog-course") and visual feedback while dragging.
  • Added catalog-drop handling to the workspace DnD provider, including a DragOverlay ghost and UUID assignment for new planner entries.
  • Expanded planner semester drop acceptance and improved button accessibility attributes across several components.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/features/toolbox/GarbageBin.tsx Adds button semantics (type, aria-label) to the garbage dropzone.
src/features/planner/components/semester/SemesterBlock.tsx Allows semesters to accept "catalog-course" drops; adds button semantics/labels.
src/features/dnd/dnd.tsx Implements catalog-to-planner drop logic and a planner-style DragOverlay ghost.
src/features/catalog/components/CatalogCourse.tsx Wires catalog cards as drag sources via useDraggable and dims while dragging.
src/components/course/Course.tsx Adds button semantics/labels to planner course drag handle and options trigger.
Comments suppressed due to low confidence (2)

src/features/dnd/dnd.tsx:191

  • handleDragEnd resets planner/toolbox state on cancel/no-target using snapshots. For catalog drags, state is not mutated during onDragOver, so this reset can unnecessarily revert unrelated in-flight workspace changes that happened while dragging. Consider skipping the snapshot reset for source.data.type === "catalog-course", or only snapshot/reset for drag types that actually perform optimistic updates during drag.
      const { source, target } = event.operation;

      if (event.canceled || !target) {
        resetPlanner(snapshotPlanner.current);
        resetToolbox(snapshotToolbox.current);
        return;

src/features/toolbox/GarbageBin.tsx:56

  • When showBin is false, the button is visually hidden via opacity-0 and pointer-events-none, but it remains in the tab order and discoverable to screen readers. Consider also setting disabled/aria-hidden/tabIndex={-1} when hidden, or conditionally rendering the button only while a draggable course is active, to avoid focusing an invisible control.
    <button
      ref={ref}
      type="button"
      aria-label="Delete course"
      className={cn(
        "w-fit transition-opacity duration-200",
        showBin ? "opacity-100" : "opacity-0 pointer-events-none",
      )}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/features/dnd/dnd.tsx
Comment thread src/features/catalog/components/CatalogCourse.tsx
liug88 added 4 commits March 31, 2026 17:23
- Skip snapshots in handleDragStart for catalog drags since onDragOver is a no-op for catalog-course and no rollback is needed on drop
- Stop pointerdown propagation on the Add button wrapper to prevent the drag sensor from initiating a drag on what should be a click
@liug88
liug88 requested review from mirmirmirr and ramonechen March 31, 2026 21:30

@ramonechen ramonechen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've identified a few issues in this change:

  1. Browser local storage seems to get nuked every time a course is dragged directly from the catalog to the planner (i.e. refreshing the page after adding a course directly from the catalog will reset the entire planner). Courses still seem to be saved properly when adding to the planner via the toolbox.

  2. I don't seem to be able to drag courses from the catalog to the toolbox. I think it's natural for the user to try this, so I would like to see this addressed if it's not too complicated.

  3. The credit count in the credit selector is missing for courses that are dragged directly from the catalog (image below). This is not an issue, however, for courses dragged from the toolbox.

Image
  1. The cloned, translucent course preview does not appear when dragging courses directly from the catalog into the planner. Without this preview, it's somewhat difficult to determine where the course will be dropped exactly when I release left-click. This preview still appears normally for courses being dragged from the toolbox.

Comment thread src/features/catalog/components/CatalogCourse.tsx
Comment thread src/features/catalog/components/CatalogCourse.tsx Outdated
Comment thread src/features/catalog/components/CatalogCourse.tsx
Comment thread src/features/dnd/dnd.tsx Outdated
Comment thread src/features/dnd/dnd.tsx Outdated
Comment thread src/features/dnd/dnd.tsx Outdated
Comment thread src/features/catalog/components/CatalogCourse.tsx
liug88 added 5 commits April 3, 2026 17:27
Each CatalogCourse now generates a stable UUID on mount via useRef,
preventing ID collisions when multiple instances of the same course
are visible. Adds a MdDragIndicator icon as a visual affordance.
…redits

- Take planner/toolbox snapshots at drag start for all drag types,
  including catalog drags, so cancellation correctly restores state
- Remove early return for catalog-course in handleDragOver; catalog
  courses now live-insert into the hovered semester during drag,
  matching the toolbox-to-planner pattern and restoring the ghost
  preview and course reordering feedback
- Add missing credits field (credit_max) when constructing UserCourse
  from an APICourse during a catalog drag
Catalog card dragging is disabled on touch devices since there is
nowhere to drop. The catalog results container switches to
overflow-hidden while a catalog drag is active so the AutoScroller
cannot scroll the list in a direction that has no valid drop targets.
The catalog card's draggable ID (stable per mount) was being used as
the planner course ID. After the first drop, courseLocationMap found
that ID in the planner and treated every subsequent drag from the same
card as a move rather than a new insertion.

Fix: generate a fresh UUID in handleDragStart for each catalog drag
session and use that as the planner course ID instead of sourceId.
- Move useIsDesktop import before @/lib/types to satisfy import-x/order
- Remove addCourseToSemester from handleDragEnd deps array; it is no
  longer called there after the catalog logic moved to handleDragOver
@liug88
liug88 requested a review from mirmirmirr April 7, 2026 20:28
Comment thread src/features/catalog/components/CatalogCourse.tsx Outdated
Comment thread src/features/dnd/dnd.tsx Outdated
Comment on lines +104 to +121
// --- CATALOG TO PLANNER ---
if (sourceType === "catalog-course") {
const apiCourse = source.data?.course as APICourse | undefined;
if (!apiCourse) return;

const targetSemesterId =
targetType === "semester"
? target.data?.semesterId || targetId
: courseLocationMap.get(targetId)?.semesterId;

if (!targetSemesterId) return;

let catalogTargetIndex: number | undefined;
if (isSortable(target)) {
catalogTargetIndex = target.index;
} else {
const targetSemester =
plannerCourses[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would reference the behavior of the course preview when dragging a course from the toolbox into the planner. Dragging a course from the catalog into the planner should do the same thing for consistency's sake.

Dragging from the toolbox into a semester shows a preview of the course inside that semester based on the drag position of that dragged course. It's not until the user stops dragging that the course is visually added officially. Right now, it seems like it is just added officially.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I am not sure where this is coming from but there seems to be some type of shadow on the semester dropzone when hovering a course from the catalog over a semester. This is not present when for courses that originate from the toolbox so it should also be removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transparent placeholder and the semester shadow are both addressed in commit 91586b7: isCatalogPlaceholder in Course.tsx makes the in-planner placeholder opacity-0 to match toolbox-drag behaviour, and the semester bg-black/5 highlight is suppressed while a catalog drag is active.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shadow on the semester dropzone is suppressed during catalog drags via the isDraggingCatalog check in SemesterBlock.tsx, committed in 91586b7.

Comment thread src/features/catalog/components/CatalogResults.tsx
Comment thread src/features/dnd/dnd.tsx Outdated
liug88 added 2 commits April 10, 2026 14:06
…uring drag

- MdDragIndicator is only rendered on desktop since mobile dragging is disabled
- Apply overflow-hidden to the inner results div during catalog drags, not
  just the outer container, so the AutoScroller cannot scroll the list
… opacity

- Extract CatalogDragOverlay into a child component of DragDropProvider
  that reads useDragOperation directly, eliminating the React state cycle
  that caused a visible delay before the drag overlay appeared
- Expose the in-flight catalog planner course ID via CatalogDragContext
  so Course.tsx can apply opacity-0 to the inserted placeholder, matching
  the ghost preview behavior of toolbox-to-planner drags
- Suppress the semester dropzone bg-black/5 highlight during catalog drags;
  the highlight was absent for toolbox drags because those transition to
  targeting individual planner-course sortables, but catalog drags always
  kept the semester droppable as the active target
@liug88

liug88 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Copilot AI and others added 2 commits April 14, 2026 20:48
Co-authored-by: liug88 <85533492+liug88@users.noreply.github.com>
- Fix: catalog drags no longer reset the planner on drop; only an
  explicit Escape cancels and undoes the placed placeholder
- Add: catalog courses can now be dragged directly into the Toolbox;
  hovering over the toolbox removes the planner placeholder for visual
  clarity, and confirming the drop smart-adds the course to the toolbox
  (increments count for duplicates)
- Also include catalog-course in the Toolbox's accept list

Agent-Logs-Url: https://github.com/Project-CARPI/site/sessions/294eba98-1f9d-4cc4-9196-c142d01155a5

Co-authored-by: liug88 <85533492+liug88@users.noreply.github.com>

Copilot AI commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged main-preview into the branch and resolved the conflict in CatalogCourse.tsx in commit 61e085a.

I've identified a few issues in this change:

  1. Browser local storage seems to get nuked every time a course is dragged directly from the catalog to the planner (i.e. refreshing the page after adding...

All four points addressed in commit 1965e4d:

  1. localStorage reset fixedhandleDragEnd now has an explicit catalog-course path. Placement from handleDragOver is committed as-is; only an explicit Escape key cancels and restores the snapshot. The previous !target check no longer resets the planner for catalog drops.

  2. Catalog → Toolbox drag added — the Toolbox now accepts "catalog-course" drops. Hovering over the Toolbox while dragging a catalog course removes the planner placeholder (visual feedback), and releasing smart-adds the course to the Toolbox (incrementing the count for duplicates).

  3. Credits fieldcredits: apiCourse.credit_max was already added to the UserCourse in an earlier commit, so the credit selector shows the correct value.

  4. Translucent preview — the isCatalogPlaceholder mechanism in Course.tsx (commit 91586b7) gives the in-planner card opacity-0 while dragging, matching the toolbox-drag behaviour.

liug88 and others added 2 commits April 17, 2026 17:31
Hopefully, this should fix the rest of the changes.
…ract

Addresses the review comment about the catalog->planner and
toolbox->planner drag paths diverging visually.

The catalog drag mounts a real <Course /> into planner state during
hover as the in-flight placeholder. Previously that element was styled
with ad-hoc Tailwind (opacity-50, pointer-events-none, and
conditional hover:shadow-lg suppression) to mimic the toolbox ghost.
The toolbox path gets the same look for free from dnd-kit, which
emits this contract on its placeholder clones:

  data-dnd-placeholder="clone" inert aria-hidden="true" tabindex="-1"

That contract, combined with the `[data-dnd-placeholder="clone"]
{ opacity: 0.5 !important }` rule already present in index.css and
the `inert` attribute, handles all four concerns uniformly:
0.5 opacity, no pointer events, no hover shadow, no keyboard focus.

This commit makes PlannerCourseView emit the same attributes when
rendering as the catalog placeholder (new `isCatalogPlaceholder`
prop) and drops the Tailwind overrides. The two flows now share the
same rendering contract -- inspect the DOM mid-drag and the two
placeholders are identical in attributes, driven by the same CSS
rule.

The split between useSortable (toolbox) and useDraggable (catalog)
stays intentional -- they describe genuinely different data sources
(stateful list item vs. read-only template) -- but the rendering
layer now converges.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread src/features/dnd/CatalogDragContext.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what the purpose of this context is. Catalog dragging should be handle by the global DragDropProvider. Placeholders should also be able to be made visible from this as well. Please reference how it is done with the other drag and drop operations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the insight! Just refactored. I Deleted CatalogDragContext entirely since the global DragDropProvider already publishes the active operation through useDragOperation(), so there's no reason to layer a second context on top. handleDragStart now just stashes a fresh placeholderId on source.data, and every consumer (Course, handleDragOver, handleDragEnd) reads it off the operation the DragDropProvider exposes. Should be the same pattern the rest of the DnD already uses.

I also tried to fix the missing toolbox placeholder: when a catalog drag hovers the toolbox, handleDragOver now inserts a ghost UserCourse there (and reorders it on further hover), mirroring the planner behavior. A shared getCatalogPlaceholderAttrs helper applies data-dnd-placeholder="clone" + inert to both planner and toolbox variants of Course, so the ghost styling is uniform regardless of which container the placeholder is in.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the lack of a planner course placeholder, I also just noticed that there is a lack of a toolbox course placeholder when hovering a course that came from the catalog over the toolbox.

liug88 added 2 commits April 24, 2026 12:58
This should be the final fix for the over using the DragDropProvider.
@liug88
liug88 requested a review from mirmirmirr April 24, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants