feat(webv2): direct-input media widgets for ImageField and VideoField workflow fields - #67
Open
lstein wants to merge 3 commits into
Open
feat(webv2): direct-input media widgets for ImageField and VideoField workflow fields#67lstein wants to merge 3 commits into
lstein wants to merge 3 commits into
Conversation
… workflow fields VideoField was connection-only in the workflow editor - and since the Video primitive's own field is a VideoField, there was no way to introduce a video into a graph at all (every video-consuming node: Frame Range from Video, Concatenate Videos, Wan/MiniMax video nodes). ImageField had a widget but no thumbnail, drop target, or upload path. Both now share a MediaInput widget: - thumbnail + name of the current item; - 'Use gallery selection' (kind-matched: a selected image is not offered to a video field); - 'Upload' via a hidden file picker -> gallery upload (image/video routes) to the gallery's selected board, then adopts the uploaded item; account-scope guarded like the rest of the app's async effects; - the whole row is a dnd-kit drop target for a single dragged gallery item of the matching kind (multi-item drags are rejected outright rather than keeping an arbitrary first item), with a DropZone overlay during eligible drags. Instance-unique drop ids keep the node editor and Linear UI panel from colliding when both render the same field. VideoField joins the stateful field set and gains value validation (video_name), making it exposable in Linear UI forms like ImageField. New gallery surface: isGalleryVideoDragData, galleryVideoUrls, galleryTransfers.uploadVideo (all thin re-exports of existing internals). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lstein
requested review from
JPPhoto,
Pfannkuchensack and
blessedcoolant
as code owners
August 7, 2026 19:29
…gets Match the legacy editor's ImageField/VideoField widgets: a full-width 128px preview showing the item's frame thumbnail at objectFit-contain size (the whole area is the drop target), a dimensions badge (WxH, plus duration for videos, resolved best-effort via the gallery item lookup), a dashed drop-hint empty state, and a media icon fallback when the thumbnail 404s. The selection/upload/clear buttons move to a compact row below the preview. No changes to the drop/upload/adoption mechanics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the legacy editor's VideoFrameIndexFieldInput to webv2: integer fields marked ui_component=video-frame-index (frame_index on Frame from Video; start_frame/end_frame on Frame Range from Video) render the number input plus a live frame preview (a muted <video> seeked to frame / fps) and a scrubber slider, all writing the same field value. The widget reads its companion 'video' field from the same node via a new optional nodeId prop on WorkflowFieldInput (both the node editor and the Linear UI panel pass it), and resolves fps/duration/URL through the same gallery item lookup the media widget's badge uses. Negative indices (-1 = last frame) are resolved for display only; the field value is preserved verbatim. Degradations (adversarial-review findings): a connection-driven video field suppresses the scrubber via resolved edges instead of previewing the stale stored value (the legacy widget gets this wrong); single-frame videos render the preview without a slider (min === max is NaN% CSS in zag); deleted videos get a 'could not be loaded' hint instead of a frame-rate story; empty-string video names never fire a lookup (also fixed in the media widget's badge query). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Found while testing the MiniMax H3 port (#56–#66) in the webv2 workflow editor, but this is core editor functionality affecting every media-consuming node (Wan video nodes included):
VideoFieldinputs rendered as "Connection only" — and since the Video primitive's own field is aVideoField, there was no way to introduce a video into a graph at all. Every video node (Frame Range from Video, Concatenate Videos, Wan i2v/extend, MiniMax H3) was a dead end.ImageFieldhad a minimal widget — filename text plus "Use gallery selection", with no thumbnail, no drop target, no upload.ui_component=video-frame-index(used by Frame from Video'sframe_indexand Frame Range from Video'sstart_frame/end_frame) parsed tonull, so those fields rendered as bare number inputs with no way to see which frame you were picking.Both media field types now share one
MediaInputwidget:/images/upload,/videos/upload) targeting the gallery's selected board, then the field adopts the uploaded item. Async work is account-scope guarded like the rest of the app.DropZoneoverlay during eligible drags. Multi-item drags are rejected outright rather than silently keeping an arbitrary first item. Drop ids are instance-unique so the node editor and Linear UI panel can render the same field simultaneously.VideoFieldjoins the stateful field set withvideo_namevalue validation, which also makes it exposable in Linear UI forms exactly likeImageField.And
video-frame-indexinteger fields get the legacy editor's frame scrubber: the number input plus a live frame preview (a muted<video>seeked toframe / fps— the browser decodes the exact frame natively) and a slider, all writing the same field value. The widget reads its companionvideofield from the same node via a new optionalnodeIdprop onWorkflowFieldInput(both the node editor and the Linear UI panel pass it, so scrubbing works for exposed form fields too). Negative indices (-1= last frame) are resolved for display only; the stored value is preserved verbatim and the backend stays authoritative.New gallery surface (thin re-exports of existing internals):
galleryVideoUrls,galleryTransfers.uploadVideo.Review
Each commit got a fresh-context adversarial review; all confirmed findings are fixed in this diff.
Media widgets round — attacked cross-monitor drop interference (field vs board vs canvas handlers on the same DragEnd), drop-id collisions (editor + Linear panel, StrictMode), the upload lifecycle, image-widget regressions through the legacy selection fallbacks, and every consumer of the stateful/exposable/validation helpers:
onChangedispatches into the active project, so an upload finishing after a project switch would churn the other project's graph, auto-flip its invoke route (setFieldValueis a high-confidence edit), and silently drop the adopted value — same class of bug the canvas import already pins against. Adoption is now gated on the widget still being mounted and the project id captured at upload start; otherwise an info toast points at the gallery. This also covers "node deleted mid-upload".invalidateGallerylike every other upload path, so the uploaded file actually appears on the board it was sent to.videos) reachable through legacyexposedFieldsmigration would have rendered the single-value widget and written a bare object into a list; and the newvideo_namevalidation would have flipped persisted array values from valid to invalid. Both media widget cases now render connection-only for COLLECTION, and validation keeps the generic non-null check for COLLECTION values (this also fixes the same latent defect for ImageField arrays).onError, name still shown); files picked through "All Files" with a mismatched MIME type are rejected client-side instead of round-tripping to a server 422; the unused video drag-data exports were dropped.Frame scrubber round — attacked
nodeIdstaleness across delete/paste/undo, the react-query cache entry shared with the media badge, seek-effect listener lifecycle, slider math edges, controlled-slider feedback loops, undo-history flooding while scrubbing, and react-flow drag/keyboard capture:videofields previewed a stale video (MED): connecting an upstream video does not clear the field's stored value, so the scrubber kept previewing (and clamping against) the old video while invoke used the upstream one — with the video widget itself hidden, there was no cue. The scrubber now consults resolved edges (the same "connected" signal the editor and Linear panel use) and shows an explicit hint instead. The legacy editor's widget has this bug; the port fixes it.min === max, which zag renders as NaN% CSS — the preview now renders without a slider; a deleted video got a misleading "no probed frame rate" hint — it now says the video could not be loaded; empty-stringvideo_namefired a guaranteed-404 lookup (/videos/i//…) — treated as unset, and the same latent flaw was fixed in the media widget's badge query.preload="auto"fetches the video per scrubber instance (legacy parity — the backend serves Range requests); a value scrubbed on a long video then re-pointed at a shorter one displays clamped while the document keeps the raw value (the number input shows it, and the backend validates at invoke).Deliberately unchanged: uploads while a date board is selected land in Uncategorized (the backend's own mapping) rather than refusing like the gallery grid does — with the invalidation fix the result is at least visible; happy to align it with the grid's refusal if preferred.
Testing
pnpm test,pnpm test:browser(491 incl. 13 new: media widget render/selection/badge/clear/upload/type-mismatch/COLLECTION, scrubber render + negative-index resolution, keyboard scrub writes the index, connection-driven suppression, single-frame no-slider, deleted-video hint, empty-name guard),pnpm lint(format, oxc, tsc, architecture check) — all green;vite buildclean.VideoFieldvalue validation, andui_componentparsing passthrough.start_frame/end_frameand confirm the previewed frames match the extracted range.🤖 Generated with Claude Code