Ci/add dependabot config - #90
Conversation
|
@Myparadox-creator is attempting to deploy a commit to the itzzavdhesh's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
This links your work to the issue and auto-closes it once merged — it's also how ELUSOC tracks contributions. |
📝 WalkthroughWalkthroughThe PR adds global keyboard shortcuts, graph-aware navigation, block duplication, shortcut help, and a consolidated export menu. It also updates canvas layout styling, sidebar stacking, and adds weekly Dependabot updates for npm packages and GitHub Actions. ChangesEditor interactions
Dependency update automation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/App.tsx (1)
320-330: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize the handlers passed into
useKeyboardShortcuts.
handleDeleteBlock,handleDuplicateBlock,handleSaveWorkspace, and the inlineonToggleShortcutsHelparrow function are all re-created on every render ofApp. SinceuseKeyboardShortcutslists these in itsuseEffectdependency array, the hook's window listener is removed and re-attached on everyApprender, not just when the underlying logic changes.Line 523 also creates a second, separate inline function with the same body as line 328. Wrap the handlers in
useCallbackand share a single toggle function between both call sites.const toggleShortcutsHelp = useCallback(() => setShowShortcutsHelp((prev) => !prev), []);Reuse
toggleShortcutsHelpat both line 328 and line 523, and wraphandleDeleteBlock,handleDuplicateBlock, andhandleSaveWorkspacesimilarly.Also applies to: 522-523
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/App.tsx` around lines 320 - 330, Memoize the handlers used by useKeyboardShortcuts to prevent unnecessary listener re-registration. Wrap handleDeleteBlock, handleDuplicateBlock, and handleSaveWorkspace in useCallback, create one shared toggleShortcutsHelp callback using the existing state updater, and reuse it both in the hook options and the second call site around the other referenced location.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/App.tsx`:
- Around line 256-286: Update handleDuplicateBlock so the setBlocks updater
derives the source block from prev and constructs a fresh duplicatedBlock
entirely inside the updater. Avoid mutating the block created outside the
callback or relying on the outer original value; preserve insertion after the
source block, targetId handling, and existing selection/toast behavior.
In `@src/hooks/useKeyboardShortcuts.ts`:
- Around line 30-47: The useKeyboardShortcuts handler must account for open
modals: pass an isModalOpen state derived from showConfirmModal or
showShortcutsHelp, route Escape to close the active modal before clearing
selection, and short-circuit destructive or duplication shortcuts such as
Delete/Backspace and Ctrl+D while a modal is open. Update the
useKeyboardShortcuts API and its App.tsx/CenterCanvas.tsx callers while
preserving normal shortcut behavior when no modal is active.
- Around line 93-167: Broaden the focus guard in isInput so keyboard
interception is skipped for all interactive controls, including buttons, links,
and other natively focusable elements, not only form fields and contentEditable
nodes. Apply this guard before both the Tab cycling block and the Arrow-key
navigation block, preserving custom navigation only when focus is outside
interactive controls.
---
Nitpick comments:
In `@src/App.tsx`:
- Around line 320-330: Memoize the handlers used by useKeyboardShortcuts to
prevent unnecessary listener re-registration. Wrap handleDeleteBlock,
handleDuplicateBlock, and handleSaveWorkspace in useCallback, create one shared
toggleShortcutsHelp callback using the existing state updater, and reuse it both
in the hook options and the second call site around the other referenced
location.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d36aac87-b9ba-4644-9aee-422f4443db51
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
.github/dependabot.ymlsrc/App.tsxsrc/components/CenterCanvas.tsxsrc/components/RightSidebar.tsxsrc/hooks/useKeyboardShortcuts.ts
| // Duplicate block (Ctrl+D / Cmd+D) | ||
| const handleDuplicateBlock = (id: string) => { | ||
| const original = blocks.find((b) => b.id === id); | ||
| if (!original) return; | ||
|
|
||
| const newId = `block-${Math.random().toString(36).substring(2, 9)}`; | ||
| const duplicatedBlock: Block = { | ||
| ...original, | ||
| id: newId, | ||
| label: `${original.label} (Copy)`, | ||
| }; | ||
|
|
||
| setBlocks((prev) => { | ||
| const idx = prev.findIndex((b) => b.id === id); | ||
| if (idx === -1) return [...prev, duplicatedBlock]; | ||
|
|
||
| const updated = [...prev]; | ||
| if (original.type !== 'decision') { | ||
| duplicatedBlock.targetId = original.targetId; | ||
| updated[idx] = { ...original, targetId: newId }; | ||
| } | ||
|
|
||
| updated.splice(idx + 1, 0, duplicatedBlock); | ||
| return updated; | ||
| }); | ||
|
|
||
| setSelectedBlockId(newId); | ||
| setActiveParentId(newId); | ||
| showToast(`Duplicated "${original.label}"`, 'success'); | ||
| }; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Compute the duplicate entirely from prev inside the updater.
duplicatedBlock is created outside setBlocks and then mutated at line 274 inside the updater callback. React may invoke a state updater more than once (for example under Strict Mode double-invocation or concurrent re-renders), and updaters should be pure functions of prev. Reading original from the outer closure also means the block could be stale if state changed between the initial blocks.find and the update.
🔧 Proposed fix
const newId = `block-${Math.random().toString(36).substring(2, 9)}`;
- const duplicatedBlock: Block = {
- ...original,
- id: newId,
- label: `${original.label} (Copy)`,
- };
setBlocks((prev) => {
const idx = prev.findIndex((b) => b.id === id);
- if (idx === -1) return [...prev, duplicatedBlock];
+ if (idx === -1) return prev;
+ const original = prev[idx];
+ const duplicatedBlock: Block = {
+ ...original,
+ id: newId,
+ label: `${original.label} (Copy)`,
+ };
const updated = [...prev];
if (original.type !== 'decision') {
- duplicatedBlock.targetId = original.targetId;
updated[idx] = { ...original, targetId: newId };
}
updated.splice(idx + 1, 0, duplicatedBlock);
return updated;
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Duplicate block (Ctrl+D / Cmd+D) | |
| const handleDuplicateBlock = (id: string) => { | |
| const original = blocks.find((b) => b.id === id); | |
| if (!original) return; | |
| const newId = `block-${Math.random().toString(36).substring(2, 9)}`; | |
| const duplicatedBlock: Block = { | |
| ...original, | |
| id: newId, | |
| label: `${original.label} (Copy)`, | |
| }; | |
| setBlocks((prev) => { | |
| const idx = prev.findIndex((b) => b.id === id); | |
| if (idx === -1) return [...prev, duplicatedBlock]; | |
| const updated = [...prev]; | |
| if (original.type !== 'decision') { | |
| duplicatedBlock.targetId = original.targetId; | |
| updated[idx] = { ...original, targetId: newId }; | |
| } | |
| updated.splice(idx + 1, 0, duplicatedBlock); | |
| return updated; | |
| }); | |
| setSelectedBlockId(newId); | |
| setActiveParentId(newId); | |
| showToast(`Duplicated "${original.label}"`, 'success'); | |
| }; | |
| // Duplicate block (Ctrl+D / Cmd+D) | |
| const handleDuplicateBlock = (id: string) => { | |
| const original = blocks.find((b) => b.id === id); | |
| if (!original) return; | |
| const newId = `block-${Math.random().toString(36).substring(2, 9)}`; | |
| setBlocks((prev) => { | |
| const idx = prev.findIndex((b) => b.id === id); | |
| if (idx === -1) return prev; | |
| const original = prev[idx]; | |
| const duplicatedBlock: Block = { | |
| ...original, | |
| id: newId, | |
| label: `${original.label} (Copy)`, | |
| }; | |
| const updated = [...prev]; | |
| if (original.type !== 'decision') { | |
| updated[idx] = { ...original, targetId: newId }; | |
| } | |
| updated.splice(idx + 1, 0, duplicatedBlock); | |
| return updated; | |
| }); | |
| setSelectedBlockId(newId); | |
| setActiveParentId(newId); | |
| showToast(`Duplicated "${original.label}"`, 'success'); | |
| }; |
🧰 Tools
🪛 React Doctor (0.9.1)
[error] 268-268: This state updater performs the captured value "duplicatedBlock". React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.
Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.
(no-impure-state-updater)
[error] 274-274: This side-effecting call runs inside a state updater, which React may invoke more than once. Move it outside the setter after computing the next state.
React may replay a state updater, so callbacks, analytics, and persistence inside it can run more than once. Compute state purely, then perform the side effect outside the setter.
(no-side-effect-in-state-updater-function)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/App.tsx` around lines 256 - 286, Update handleDuplicateBlock so the
setBlocks updater derives the source block from prev and constructs a fresh
duplicatedBlock entirely inside the updater. Avoid mutating the block created
outside the callback or relying on the outer original value; preserve insertion
after the source block, targetId handling, and existing selection/toast
behavior.
Source: Linters/SAST tools
| useEffect(() => { | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| const target = e.target as HTMLElement | null; | ||
| const isInput = | ||
| target && | ||
| (target.tagName === 'INPUT' || | ||
| target.tagName === 'TEXTAREA' || | ||
| target.tagName === 'SELECT' || | ||
| target.isContentEditable); | ||
|
|
||
| // Escape works everywhere to clear selection or blur input | ||
| if (e.key === 'Escape') { | ||
| if (isInput) { | ||
| target.blur(); | ||
| } | ||
| onSelectBlock(null); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Give the handler awareness of open modals.
handleKeyDown has no knowledge of showConfirmModal or showShortcutsHelp from App.tsx/CenterCanvas.tsx. Two consequences follow:
- Escape (lines 41-47) only calls
onSelectBlock(null). It never closes the Keyboard Shortcuts modal or the New Flowchart confirmation dialog. A user who presses Escape expecting to dismiss the open modal instead silently clears the block selection. - Delete/Backspace (lines 85-91) and Ctrl+D duplicate (lines 76-82) still fire while a modal is open. A user can delete or duplicate the currently selected block while only intending to interact with the confirmation dialog behind it.
Pass the relevant modal-open state (for example isModalOpen: showConfirmModal || showShortcutsHelp) into useKeyboardShortcuts and short-circuit most shortcuts when a modal is open, while routing Escape to close the active modal instead of clearing selection.
Also applies to: 75-91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useKeyboardShortcuts.ts` around lines 30 - 47, The
useKeyboardShortcuts handler must account for open modals: pass an isModalOpen
state derived from showConfirmModal or showShortcutsHelp, route Escape to close
the active modal before clearing selection, and short-circuit destructive or
duplication shortcuts such as Delete/Backspace and Ctrl+D while a modal is open.
Update the useKeyboardShortcuts API and its App.tsx/CenterCanvas.tsx callers
while preserving normal shortcut behavior when no modal is active.
| // Tab / Shift+Tab: Cycle through blocks | ||
| if (e.key === 'Tab') { | ||
| if (blocks.length === 0) return; | ||
| e.preventDefault(); | ||
|
|
||
| const currentIndex = blocks.findIndex((b) => b.id === selectedBlockId); | ||
| if (currentIndex === -1) { | ||
| onSelectBlock(blocks[0].id); | ||
| } else { | ||
| let nextIndex: number; | ||
| if (e.shiftKey) { | ||
| nextIndex = (currentIndex - 1 + blocks.length) % blocks.length; | ||
| } else { | ||
| nextIndex = (currentIndex + 1) % blocks.length; | ||
| } | ||
| onSelectBlock(blocks[nextIndex].id); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // Arrow Keys: Smart Graph & Diagram Navigation | ||
| if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { | ||
| if (blocks.length === 0) return; | ||
| e.preventDefault(); | ||
|
|
||
| if (!selectedBlockId) { | ||
| onSelectBlock(blocks[0].id); | ||
| return; | ||
| } | ||
|
|
||
| const currentBlock = blocks.find((b) => b.id === selectedBlockId); | ||
| if (!currentBlock) return; | ||
|
|
||
| if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { | ||
| // Move to downstream target | ||
| if (currentBlock.type === 'decision') { | ||
| const nextId = | ||
| e.key === 'ArrowRight' | ||
| ? currentBlock.yesTargetId || currentBlock.noTargetId | ||
| : currentBlock.noTargetId || currentBlock.yesTargetId; | ||
| if (nextId) { | ||
| onSelectBlock(nextId); | ||
| return; | ||
| } | ||
| } else if (currentBlock.targetId) { | ||
| onSelectBlock(currentBlock.targetId); | ||
| return; | ||
| } | ||
|
|
||
| // Fallback to next block in array if no explicit graph link | ||
| const idx = blocks.findIndex((b) => b.id === selectedBlockId); | ||
| if (idx !== -1 && idx < blocks.length - 1) { | ||
| onSelectBlock(blocks[idx + 1].id); | ||
| } | ||
| } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { | ||
| // Move to upstream parent node (node that points to currentBlock.id) | ||
| const parent = blocks.find( | ||
| (b) => | ||
| b.targetId === currentBlock.id || | ||
| b.yesTargetId === currentBlock.id || | ||
| b.noTargetId === currentBlock.id | ||
| ); | ||
|
|
||
| if (parent) { | ||
| onSelectBlock(parent.id); | ||
| return; | ||
| } | ||
|
|
||
| // Fallback to previous block in array | ||
| const idx = blocks.findIndex((b) => b.id === selectedBlockId); | ||
| if (idx > 0) { | ||
| onSelectBlock(blocks[idx - 1].id); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Tab (and Arrow) key hijack breaks native focus navigation.
isInput only recognizes INPUT, TEXTAREA, SELECT, and contentEditable elements. Any other focused element, including a <button>, is treated as "not typing" and falls through to the Tab handler at line 94, which calls e.preventDefault() whenever blocks.length > 0.
Native Tab-out-of-a-field works only while focus is still inside a form field, because the isInput check at line 64 returns before reaching this block. Once a user tabs from a field onto any <button> or other non-form element, every subsequent Tab press is intercepted by this handler instead of moving focus to the next control. Keyboard-only and screen-reader users then cannot reach the toolbar, the export dropdown, or the sidebar controls in CenterCanvas.tsx and RightSidebar.tsx — this is a partial keyboard trap.
The same gap affects Arrow keys (lines 113-167).
Scope the interception so it does not apply when focus is on an interactive control:
🔧 Proposed fix
// Tab / Shift+Tab: Cycle through blocks
if (e.key === 'Tab') {
if (blocks.length === 0) return;
+ // Do not hijack Tab when focus is on an interactive control;
+ // otherwise keyboard users cannot Tab past the first button.
+ const isFocusableControl =
+ target &&
+ (target.tagName === 'BUTTON' ||
+ target.tagName === 'A' ||
+ target.hasAttribute('tabindex'));
+ if (isFocusableControl) return;
e.preventDefault();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useKeyboardShortcuts.ts` around lines 93 - 167, Broaden the focus
guard in isInput so keyboard interception is skipped for all interactive
controls, including buttons, links, and other natively focusable elements, not
only form fields and contentEditable nodes. Apply this guard before both the Tab
cycling block and the Arrow-key navigation block, preserving custom navigation
only when focus is outside interactive controls.
There was a problem hiding this comment.
7 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/components/CenterCanvas.tsx">
<violation number="1" location="src/components/CenterCanvas.tsx:408">
P2: Export dropdown is clipped below the 64px toolbar, so its PNG/PDF/PPTX/JSON choices cannot be clicked. Render the menu outside the horizontally scrolling header (or use a portal) so horizontal scrolling does not create a vertical clipping container.</violation>
<violation number="2" location="src/components/CenterCanvas.tsx:928">
P2: Opening Shortcuts Help leaves diagram keyboard commands active and prevents Tab from reaching its close controls; Delete/arrow keys can modify or navigate the underlying diagram. Suspend diagram shortcuts while this dialog is open, handle Escape to close it, and place focus inside the dialog.</violation>
</file>
<file name="src/hooks/useKeyboardShortcuts.ts">
<violation number="1" location="src/hooks/useKeyboardShortcuts.ts:76">
P2: Duplicating a selected block also triggers the browser bookmark shortcut because this handler never cancels the key event. Prevent the default before duplicating so Ctrl/Cmd+D remains an app-only shortcut.</violation>
<violation number="2" location="src/hooks/useKeyboardShortcuts.ts:94">
P2: Once any block exists (blocks.length > 0, which is almost always), this global window keydown handler calls e.preventDefault() on every Tab / Shift+Tab keystroke anywhere in the document and uses it to cycle block selection instead. The isInput guard only exempts INPUT/TEXTAREA/SELECT/contentEditable, so focus can never be moved between the toolbar/sidebar buttons and links with the keyboard, breaking keyboard operability (WCAG) across the whole app. The arrow-key handler similarly hijacks all arrow keys globally. Consider restricting these navigation shortcuts to when focus is actually on the canvas (e.g. canvas wrapper focused) and not over any focusable interactive element, or skip them when the event target is a button/link.</violation>
</file>
<file name=".github/dependabot.yml">
<violation number="1" location=".github/dependabot.yml:13">
P3: The automated labeling this config claims to set up won't take effect: Dependabot only applies labels that already exist in the repository, and none of `dependencies`, `javascript`, or `github-actions` are defined in `.github/labels.yml` (the manifest synced to GitHub by `label-sync.yml`). Consider adding these labels to `.github/labels.yml` (e.g. `dependencies`, `javascript`, `github-actions` with a color/description each), or pointing `labels` at existing labels, so Dependabot PRs are actually tagged as intended.</violation>
</file>
<file name="src/App.tsx">
<violation number="1" location="src/App.tsx:268">
P2: `duplicatedBlock` is built from the outer `original` (captured via closure) and then mutated inside the `setBlocks` updater. State updaters should be pure and derive all data from `prev`; since React can invoke updaters more than once, compute `original` and `duplicatedBlock` from `prev` inside the updater instead of relying on the outer closure to avoid stale-state duplication bugs.</violation>
<violation number="2" location="src/App.tsx:273">
P2: Duplicating a decision block leaves the copy disconnected from the graph. The non-decision branch rewires the original to point at the new copy (`updated[idx] = { ...original, targetId: newId }`), but for decision nodes that branch is skipped, so the copy keeps the original's yesTargetId/noTargetId while no node points back to it. The copy still renders on the canvas but is unreachable via the new arrow-key graph navigation and behaves inconsistently with the non-decision duplicate. Consider rewiring the copied decision node's incoming parent(s) to the copy as well so the duplicate is actually part of the diagram.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| )} | ||
|
|
||
| {/* Keyboard Shortcuts Help Modal */} | ||
| {showShortcutsHelp && ( |
There was a problem hiding this comment.
P2: Opening Shortcuts Help leaves diagram keyboard commands active and prevents Tab from reaching its close controls; Delete/arrow keys can modify or navigate the underlying diagram. Suspend diagram shortcuts while this dialog is open, handle Escape to close it, and place focus inside the dialog.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/CenterCanvas.tsx, line 928:
<comment>Opening Shortcuts Help leaves diagram keyboard commands active and prevents Tab from reaching its close controls; Delete/arrow keys can modify or navigate the underlying diagram. Suspend diagram shortcuts while this dialog is open, handle Escape to close it, and place focus inside the dialog.</comment>
<file context>
@@ -878,6 +923,60 @@ export default function CenterCanvas({
)}
+
+ {/* Keyboard Shortcuts Help Modal */}
+ {showShortcutsHelp && (
+ <div className="fixed inset-0 bg-slate-900/50 backdrop-blur-xs z-50 flex items-center justify-center p-4 animate-fade-in">
+ <div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-gray-100 dark:border-slate-700 max-w-md w-full p-6 transform transition-all scale-100">
</file context>
| {/* Top Toolbar */} | ||
| <header className="h-[64px] bg-white dark:bg-slate-800 border-b border-gray-100 dark:border-slate-700 shadow-xs px-6 flex items-center justify-between shrink-0 select-none z-10"> | ||
| <div className="flex items-center gap-2"> | ||
| <header className="h-[64px] bg-white dark:bg-slate-800 border-b border-gray-100 dark:border-slate-700 shadow-xs px-4 flex items-center justify-between shrink-0 select-none z-10 overflow-x-auto overflow-y-visible custom-scrollbar flex-nowrap min-w-0 max-w-full"> |
There was a problem hiding this comment.
P2: Export dropdown is clipped below the 64px toolbar, so its PNG/PDF/PPTX/JSON choices cannot be clicked. Render the menu outside the horizontally scrolling header (or use a portal) so horizontal scrolling does not create a vertical clipping container.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/CenterCanvas.tsx, line 408:
<comment>Export dropdown is clipped below the 64px toolbar, so its PNG/PDF/PPTX/JSON choices cannot be clicked. Render the menu outside the horizontally scrolling header (or use a portal) so horizontal scrolling does not create a vertical clipping container.</comment>
<file context>
@@ -383,19 +405,19 @@ export default function CenterCanvas({
{/* Top Toolbar */}
- <header className="h-[64px] bg-white dark:bg-slate-800 border-b border-gray-100 dark:border-slate-700 shadow-xs px-6 flex items-center justify-between shrink-0 select-none z-10">
- <div className="flex items-center gap-2">
+ <header className="h-[64px] bg-white dark:bg-slate-800 border-b border-gray-100 dark:border-slate-700 shadow-xs px-4 flex items-center justify-between shrink-0 select-none z-10 overflow-x-auto overflow-y-visible custom-scrollbar flex-nowrap min-w-0 max-w-full">
+ <div className="flex items-center gap-2 shrink-0">
<span className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-slate-500">Workspace</span>
</file context>
| } | ||
|
|
||
| // Duplicate Block: Ctrl + D / Cmd + D | ||
| if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'd') { |
There was a problem hiding this comment.
P2: Duplicating a selected block also triggers the browser bookmark shortcut because this handler never cancels the key event. Prevent the default before duplicating so Ctrl/Cmd+D remains an app-only shortcut.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/useKeyboardShortcuts.ts, line 76:
<comment>Duplicating a selected block also triggers the browser bookmark shortcut because this handler never cancels the key event. Prevent the default before duplicating so Ctrl/Cmd+D remains an app-only shortcut.</comment>
<file context>
@@ -0,0 +1,184 @@
+ }
+
+ // Duplicate Block: Ctrl + D / Cmd + D
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'd') {
+ e.preventDefault();
+ if (selectedBlockId) {
</file context>
| } | ||
|
|
||
| // Tab / Shift+Tab: Cycle through blocks | ||
| if (e.key === 'Tab') { |
There was a problem hiding this comment.
P2: Once any block exists (blocks.length > 0, which is almost always), this global window keydown handler calls e.preventDefault() on every Tab / Shift+Tab keystroke anywhere in the document and uses it to cycle block selection instead. The isInput guard only exempts INPUT/TEXTAREA/SELECT/contentEditable, so focus can never be moved between the toolbar/sidebar buttons and links with the keyboard, breaking keyboard operability (WCAG) across the whole app. The arrow-key handler similarly hijacks all arrow keys globally. Consider restricting these navigation shortcuts to when focus is actually on the canvas (e.g. canvas wrapper focused) and not over any focusable interactive element, or skip them when the event target is a button/link.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/useKeyboardShortcuts.ts, line 94:
<comment>Once any block exists (blocks.length > 0, which is almost always), this global window keydown handler calls e.preventDefault() on every Tab / Shift+Tab keystroke anywhere in the document and uses it to cycle block selection instead. The isInput guard only exempts INPUT/TEXTAREA/SELECT/contentEditable, so focus can never be moved between the toolbar/sidebar buttons and links with the keyboard, breaking keyboard operability (WCAG) across the whole app. The arrow-key handler similarly hijacks all arrow keys globally. Consider restricting these navigation shortcuts to when focus is actually on the canvas (e.g. canvas wrapper focused) and not over any focusable interactive element, or skip them when the event target is a button/link.</comment>
<file context>
@@ -0,0 +1,184 @@
+ }
+
+ // Tab / Shift+Tab: Cycle through blocks
+ if (e.key === 'Tab') {
+ if (blocks.length === 0) return;
+ e.preventDefault();
</file context>
| if (idx === -1) return [...prev, duplicatedBlock]; | ||
|
|
||
| const updated = [...prev]; | ||
| if (original.type !== 'decision') { |
There was a problem hiding this comment.
P2: Duplicating a decision block leaves the copy disconnected from the graph. The non-decision branch rewires the original to point at the new copy (updated[idx] = { ...original, targetId: newId }), but for decision nodes that branch is skipped, so the copy keeps the original's yesTargetId/noTargetId while no node points back to it. The copy still renders on the canvas but is unreachable via the new arrow-key graph navigation and behaves inconsistently with the non-decision duplicate. Consider rewiring the copied decision node's incoming parent(s) to the copy as well so the duplicate is actually part of the diagram.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/App.tsx, line 273:
<comment>Duplicating a decision block leaves the copy disconnected from the graph. The non-decision branch rewires the original to point at the new copy (`updated[idx] = { ...original, targetId: newId }`), but for decision nodes that branch is skipped, so the copy keeps the original's yesTargetId/noTargetId while no node points back to it. The copy still renders on the canvas but is unreachable via the new arrow-key graph navigation and behaves inconsistently with the non-decision duplicate. Consider rewiring the copied decision node's incoming parent(s) to the copy as well so the duplicate is actually part of the diagram.</comment>
<file context>
@@ -251,6 +253,37 @@ export default function App() {
+ if (idx === -1) return [...prev, duplicatedBlock];
+
+ const updated = [...prev];
+ if (original.type !== 'decision') {
+ duplicatedBlock.targetId = original.targetId;
+ updated[idx] = { ...original, targetId: newId };
</file context>
| label: `${original.label} (Copy)`, | ||
| }; | ||
|
|
||
| setBlocks((prev) => { |
There was a problem hiding this comment.
P2: duplicatedBlock is built from the outer original (captured via closure) and then mutated inside the setBlocks updater. State updaters should be pure and derive all data from prev; since React can invoke updaters more than once, compute original and duplicatedBlock from prev inside the updater instead of relying on the outer closure to avoid stale-state duplication bugs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/App.tsx, line 268:
<comment>`duplicatedBlock` is built from the outer `original` (captured via closure) and then mutated inside the `setBlocks` updater. State updaters should be pure and derive all data from `prev`; since React can invoke updaters more than once, compute `original` and `duplicatedBlock` from `prev` inside the updater instead of relying on the outer closure to avoid stale-state duplication bugs.</comment>
<file context>
@@ -251,6 +253,37 @@ export default function App() {
+ label: `${original.label} (Copy)`,
+ };
+
+ setBlocks((prev) => {
+ const idx = prev.findIndex((b) => b.id === id);
+ if (idx === -1) return [...prev, duplicatedBlock];
</file context>
| interval: "weekly" | ||
| open-pull-requests-limit: 10 | ||
| labels: | ||
| - "dependencies" |
There was a problem hiding this comment.
P3: The automated labeling this config claims to set up won't take effect: Dependabot only applies labels that already exist in the repository, and none of dependencies, javascript, or github-actions are defined in .github/labels.yml (the manifest synced to GitHub by label-sync.yml). Consider adding these labels to .github/labels.yml (e.g. dependencies, javascript, github-actions with a color/description each), or pointing labels at existing labels, so Dependabot PRs are actually tagged as intended.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/dependabot.yml, line 13:
<comment>The automated labeling this config claims to set up won't take effect: Dependabot only applies labels that already exist in the repository, and none of `dependencies`, `javascript`, or `github-actions` are defined in `.github/labels.yml` (the manifest synced to GitHub by `label-sync.yml`). Consider adding these labels to `.github/labels.yml` (e.g. `dependencies`, `javascript`, `github-actions` with a color/description each), or pointing `labels` at existing labels, so Dependabot PRs are actually tagged as intended.</comment>
<file context>
@@ -0,0 +1,24 @@
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+ - "javascript"
+
</file context>
Description
This PR adds a
.github/dependabot.ymlconfiguration file to enable automated weekly dependency updates across the repository.Why this change?
Keeping dependencies up to date manually can lead to missed security patches and technical debt over time. Adding Dependabot ensures that both
npmpackages andgithub-actionsworkflow versions are routinely monitored and updated with minimal maintenance overhead.Summary of changes:
.github/dependabot.ymlv2 configuration.npmpackages andgithub-actions.dependencies,javascript,github-actions) and a maximum limit of 10 open PRs.Related Issue
Closes #
Type of Change
Testing Performed
npm install && npm run devstarts without errorsnpm run lintpasses with no TypeScript errorsSummary by cubic
Adds Dependabot for weekly dependency updates and introduces keyboard shortcuts with an in‑app help modal and a unified Export menu to speed up flow editing. Also tightens toolbar overflow and layering to prevent UI overlap.
New Features
useKeyboardShortcuts(Delete/Backspace, Ctrl/Cmd+A, Ctrl/Cmd+D, Ctrl/Cmd+S, Tab/Shift+Tab, Arrow keys, Escape) with a "Shortcuts" button and Shift+? help modal.Dependencies
.github/dependabot.ymlto run weekly updates fornpmandgithub-actions, apply labels, and cap to 10 open PRs.Written for commit b06d6a6. Summary will update on new commits.
Summary by CodeRabbit