Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/editor/src/components/DataClassBadges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ComponentType } from 'react'
import { ShieldBadgeIcon, LandmarkBadgeIcon, KeyBadgeIcon, EyeBadgeIcon } from './icons'

export type DataClass = 'pii' | 'financial' | 'credentials' | 'internal'

interface BadgeDef {
icon: ComponentType<{ size?: number; className?: string }>
colorVar: string
label: string
}

const BADGE_MAP: Record<DataClass, BadgeDef> = {
Comment on lines +1 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/editor was modified but no .changeset/*.md was added — should we add a .changeset file documenting the @ruminaider/flowprint-editor change?

Finding type: AI Coding Guidelines | Severity: 🟠 Medium


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/editor/src/components/DataClassBadges.tsx around lines 1-65, the new
DataClassBadges component was added but no .changeset was created. Add a new
.changeset/<describe-editor-change>.md file that documents the change for the
@ruminaider/flowprint-editor package (brief title and a short description that the
DataClassBadges component was added/updated), and specify the package release bump
(e.g., patch) in the changeset frontmatter. Ensure the changeset filename is unique,
include the package name exactly, and save the file so the editor package will be
included in the next release.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

pii: { icon: ShieldBadgeIcon, colorVar: 'var(--fp-badge-pii)', label: 'PII' },
financial: { icon: LandmarkBadgeIcon, colorVar: 'var(--fp-badge-financial)', label: 'Financial' },
credentials: {
icon: KeyBadgeIcon,
colorVar: 'var(--fp-badge-credentials)',
label: 'Credentials',
},
internal: { icon: EyeBadgeIcon, colorVar: 'var(--fp-badge-internal)', label: 'Internal' },
}

export interface DataClassBadgesProps {
/** Classifications explicitly set on this element */
explicit?: DataClass[]
/** Classifications inherited from the lane (shown at 50% opacity) */
inherited?: DataClass[]
/** Icon size in pixels (default 12) */
size?: number
}

export function DataClassBadges({
explicit = [],
inherited = [],
size = 12,
}: DataClassBadgesProps) {
const allClasses = [...new Set([...explicit, ...inherited])]
if (allClasses.length === 0) return null

return (
<div className="fp-data-badges" data-testid="data-class-badges">
{allClasses.map((dc) => {
const badge = BADGE_MAP[dc]
const Icon = badge.icon
const isExplicit = explicit.includes(dc)

return (
<span
key={dc}
className="fp-data-badge"
data-testid={`data-badge-${dc}`}
data-inherited={!isExplicit ? 'true' : undefined}
title={`${badge.label}${isExplicit ? '' : ' (inherited from lane)'}`}
style={{
color: badge.colorVar,
opacity: isExplicit ? 1 : 0.5,
}}
>
<Icon size={size} />
</span>
)
})}
</div>
)
}
29 changes: 17 additions & 12 deletions packages/editor/src/components/LaneHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState, useRef, useCallback, useEffect } from 'react'
import { GripVertical, ChevronDown, ChevronRight } from 'lucide-react'
import { DataClassBadges } from './DataClassBadges'
import type { DataClass } from './DataClassBadges'

export interface LaneHeaderProps {
laneId: string
name: string
color: string
collapsed: boolean
dataClass?: DataClass[]
onToggleCollapse: (laneId: string) => void
onRename: (laneId: string, newName: string) => void
onDragStart?: (e: React.DragEvent, laneId: string) => void
Expand All @@ -18,6 +21,7 @@ export function LaneHeader({
name,
color,
collapsed,
dataClass,
onToggleCollapse,
onRename,
onDragStart,
Expand Down Expand Up @@ -74,10 +78,7 @@ export function LaneHeader({
const CollapseIcon = collapsed ? ChevronRight : ChevronDown

return (
<div
className="fp-lane__header nopan"
style={{ '--lane-color': color } as React.CSSProperties}
>
<div className="fp-lane__header nopan" style={{ '--lane-color': color } as React.CSSProperties}>
<div
className="fp-lane__drag-handle"
draggable
Expand All @@ -93,23 +94,27 @@ export function LaneHeader({
ref={inputRef}
className="fp-lane__name-input"
value={editValue}
onChange={(e) => { setEditValue(e.target.value) }}
onChange={(e) => {
setEditValue(e.target.value)
}}
onBlur={commitRename}
onKeyDown={handleKeyDown}
aria-label="Lane name"
/>
) : (
<span
className="fp-lane__name"
onDoubleClick={handleDoubleClick}
>
{name}
</span>
<div className="fp-lane__name-group">
<span className="fp-lane__name" onDoubleClick={handleDoubleClick}>
{name}
</span>
{dataClass && dataClass.length > 0 && <DataClassBadges explicit={dataClass} />}
</div>
)}

<button
className="fp-lane__chevron"
onClick={() => { onToggleCollapse(laneId) }}
onClick={() => {
onToggleCollapse(laneId)
}}
aria-label={collapsed ? 'Expand lane' : 'Collapse lane'}
type="button"
>
Expand Down
60 changes: 60 additions & 0 deletions packages/editor/src/components/__tests__/LaneHeaderBadges.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, cleanup } from '@testing-library/react'
import { LaneHeader } from '../LaneHeader'

function renderLaneHeader(overrides?: Partial<React.ComponentProps<typeof LaneHeader>>) {
const defaults = {
laneId: 'lane-1',
name: 'Customer Actions',
color: '#3b82f6',
collapsed: false,
onToggleCollapse: vi.fn(),
onRename: vi.fn(),
}
const props = { ...defaults, ...overrides }
return { ...render(<LaneHeader {...props} />), props }
}

describe('LaneHeader – data classification badges', () => {
afterEach(() => {
cleanup()
vi.clearAllMocks()
})

it('shows data classification badges when dataClass is provided', () => {
const { container } = renderLaneHeader({ dataClass: ['pii', 'financial'] })

const badges = container.querySelector('[data-testid="data-class-badges"]')
expect(badges).toBeTruthy()
expect(container.querySelector('[data-testid="data-badge-pii"]')).toBeTruthy()
expect(container.querySelector('[data-testid="data-badge-financial"]')).toBeTruthy()
})

it('shows badges at 100% opacity (lane-level is always explicit)', () => {
const { container } = renderLaneHeader({ dataClass: ['credentials'] })

const badge = container.querySelector('[data-testid="data-badge-credentials"]')
expect(badge).toBeTruthy()
expect((badge as HTMLElement).style.opacity).toBe('1')
expect(badge?.getAttribute('data-inherited')).toBeNull()
})

it('does not show badges when dataClass is not provided', () => {
const { container } = renderLaneHeader()

expect(container.querySelector('[data-testid="data-class-badges"]')).toBeNull()
})

it('does not show badges when dataClass is empty array', () => {
const { container } = renderLaneHeader({ dataClass: [] })

expect(container.querySelector('[data-testid="data-class-badges"]')).toBeNull()
})

it('still renders lane name alongside badges', () => {
renderLaneHeader({ dataClass: ['internal'] })

expect(screen.getByText('Customer Actions')).toBeTruthy()
expect(screen.getByLabelText('Collapse lane')).toBeTruthy()
})
})
153 changes: 153 additions & 0 deletions packages/editor/src/components/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Inline SVG icon components for action modes and data classification badges.
* Uses the same props signature as lucide-react icons for compatibility with NodeShell.
*/

interface IconProps {
size?: number
className?: string
}

/* ── Action Mode Icons ──────────────────────── */

export function CalculatorIcon({ size = 16, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<rect width="16" height="20" x="4" y="2" rx="2" />
<line x1="8" x2="16" y1="6" y2="6" />
<line x1="16" x2="16" y1="14" y2="18" />
<path d="M16 10h.01" />
<path d="M12 10h.01" />
<path d="M8 10h.01" />
<path d="M12 14h.01" />
<path d="M8 14h.01" />
<path d="M12 18h.01" />
<path d="M8 18h.01" />
</svg>
)
}

export function TableIcon({ size = 16, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M12 3v18" />
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M3 9h18" />
<path d="M3 15h18" />
</svg>
)
}

/* ── Data Classification Badge Icons ────────── */

export function ShieldBadgeIcon({ size = 12, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
</svg>
)
}

export function LandmarkBadgeIcon({ size = 12, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<line x1="3" x2="21" y1="22" y2="22" />
<line x1="6" x2="6" y1="18" y2="11" />
<line x1="10" x2="10" y1="18" y2="11" />
<line x1="14" x2="14" y1="18" y2="11" />
<line x1="18" x2="18" y1="18" y2="11" />
<polygon points="12 2 20 7 4 7" />
</svg>
)
}

export function KeyBadgeIcon({ size = 12, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" />
<path d="m21 2-9.6 9.6" />
<circle cx="7.5" cy="15.5" r="5.5" />
</svg>
)
}

export function EyeBadgeIcon({ size = 12, className }: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>
)
}
Loading