Skip to content

Conversation

@luarmr
Copy link
Contributor

@luarmr luarmr commented Nov 4, 2025

Refactor: Migrate DataManager from Block/Elem to cn() Helper

Summary

This PR completes the migration of the DataManager library from the legacy Block/Elem BEM wrappers to the modern cn() helper function. This brings DataManager in line with the Editor library's architecture and modernizes the codebase.

What Changed

  • 25 component files migrated from Block/Elem to cn() helper
  • 1 test file updated to use new cn() mocks
  • bem.tsx cleaned up to remove unused exports
  • 0 files skipped - 100% migration success rate
  • 0 breaking changes - all transformations preserve equivalent class strings

Motivation

  1. Consistency: Aligns DataManager with the Editor library's BEM implementation
  2. Modernization: Moves away from legacy wrapper components to functional helpers
  3. Maintainability: Cleaner, more explicit class name generation
  4. Performance: Eliminates unnecessary component wrappers

Technical Details

Transformation Patterns

Block Components

// Before
<Block name="pagination" mod={{ size }} mix={className}>
  {children}
</Block>

// After
<div className={cn("pagination").mod({ size }).mix(className).toClassName()}>
  {children}
</div>

Elem Components

// Before
<Elem name="header" mod={{ active }}>
  {content}
</Elem>

// After
<div className={cn("parent-block").elem("header").mod({ active }).toClassName()}>
  {content}
</div>

Dynamic Tags

// Before
<Block tag={tagName} name="label">

// After
createElement(tagName, {
  className: cn("label").toClassName(),
  ...props
}, children)

Component Tags

// Before
<Elem tag={Resizer} name="container" minWidth={200} />

// After
<Resizer className={cn("parent").elem("container").toClassName()} minWidth={200} />

Files Migrated

Phase 1: Simple Components (5 files)

  • Badge.jsx
  • Tag.jsx
  • SkeletonGap.tsx
  • SkeletonLine.tsx
  • Label/Label.jsx

Phase 2: Form Components (4 files)

  • Form/Elements/Counter/Counter.jsx
  • Form/Elements/Label/Label.jsx
  • RadioGroup/RadioGroup.jsx
  • Form/Form.jsx

Phase 3: UI Components (3 files)

  • Range/Range.jsx
  • Pagination/Pagination.tsx
  • Resizer/Resizer.jsx

Phase 4: Complex Components (11 files)

  • FieldsButton.jsx
  • MediaPlayer/MediaPlayer.jsx
  • MediaPlayer/MediaSeeker.jsx
  • SkeletonLoader/SkeletonLoader.tsx
  • MainView/DataView/Table.jsx
  • MainView/GridView/GridView.jsx
  • DataManager/Toolbar/Toolbar.jsx
  • DataManager/Toolbar/ActionsButton.jsx
  • DataManager/Toolbar/instruments.jsx
  • Filters/Filters.jsx
  • Filters/FiltersSidebar/FilterSidebar.jsx

Phase 5: Final Components (1 file)

  • DataGroups/ImageDataGroup.jsx

Phase 6: Tests & Cleanup (2 tasks)

  • GridView.test.tsx - Updated mocks
  • bem.tsx - Removed unused exports

Commit Structure

Each component was migrated in an individual commit for easy review and potential rollback. All commits follow the format:

refactor(bem): replace Block/Elem with cn() in [ComponentName]

Migrated [Component] from Block/Elem to cn() helper.
- [Detailed list of changes]
- Preserved all mods, refs, handlers, and props
- No behavior change, equivalent class strings

Verification

Zero Block/Elem imports remaining

grep -r "import.*{.*Block.*}.*bem\|import.*{.*Elem.*}.*bem" src/ --include="*.tsx" --include="*.jsx"
# Result: 0 matches

Biome linting passes

yarn biome check . --diagnostic-level=error
# Result: All checks passed

All transformations preserve class strings

  • Method order maintained: cn(block).elem(elem).mod(mod).mix(mix).toClassName()
  • No CSS changes required
  • Same class names generated at runtime

Breaking Changes

None. This is a pure refactoring with no functional changes:

  • Same CSS class names generated
  • Same component behavior
  • Same props interface
  • Same styling output

Testing Recommendations

  1. Visual regression testing - Verify UI components render identically
  2. Interaction testing - Verify all click handlers, forms, and controls work
  3. Responsive testing - Verify layout and responsive behavior unchanged
  4. Browser testing - Verify across supported browsers

Migration Statistics

  • Total files: 25 component files + 1 test file + 1 config file = 27 files
  • Lines changed: ~500+ lines across all files
  • Individual commits: 26 commits + 2 fix commits = 28 total
  • Migration time: Completed in phases with automated assistance
  • Success rate: 100% (0 files skipped)

Follow-up

This completes the BEM migration for DataManager. The codebase now uses modern cn() helpers consistently across both Editor and DataManager libraries.

Related Work

@luarmr luarmr requested a review from a team as a code owner November 4, 2025 22:11
@netlify
Copy link

netlify bot commented Nov 4, 2025

Deploy Preview for label-studio-docs-new-theme canceled.

Name Link
🔨 Latest commit 41ea23f
🔍 Latest deploy log https://app.netlify.com/projects/label-studio-docs-new-theme/deploys/690b7f60dd1c9000083b2b61

@netlify
Copy link

netlify bot commented Nov 4, 2025

Deploy Preview for label-studio-storybook ready!

Name Link
🔨 Latest commit 41ea23f
🔍 Latest deploy log https://app.netlify.com/projects/label-studio-storybook/deploys/690b7f601b34800008bc4a82
😎 Deploy Preview https://deploy-preview-8757--label-studio-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 4, 2025

Deploy Preview for label-studio-playground ready!

Name Link
🔨 Latest commit 41ea23f
🔍 Latest deploy log https://app.netlify.com/projects/label-studio-playground/deploys/690b7f607456ba0008424177
😎 Deploy Preview https://deploy-preview-8757--label-studio-playground.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 4, 2025

Deploy Preview for heartex-docs canceled.

Name Link
🔨 Latest commit 41ea23f
🔍 Latest deploy log https://app.netlify.com/projects/heartex-docs/deploys/690b7f60a789ed0008330bae

@luarmr luarmr force-pushed the fb-echo-420/remove-bem-from-dm-lib branch from 2e47d45 to 39e836e Compare November 4, 2025 22:27
@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 50.69%. Comparing base (d2039fc) to head (41ea23f).
⚠️ Report is 2 commits behind head on develop.

❗ There is a different number of reports uploaded between BASE (d2039fc) and HEAD (41ea23f). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (d2039fc) HEAD (41ea23f)
pytests 1 0
lsf-e2e 1 0
Additional details and impacted files
@@             Coverage Diff              @@
##           develop    #8757       +/-   ##
============================================
- Coverage    66.72%   50.69%   -16.03%     
============================================
  Files          792      553      -239     
  Lines        61014    39319    -21695     
  Branches     10400    10400               
============================================
- Hits         40709    19932    -20777     
+ Misses       20302    19384      -918     
  Partials         3        3               
Flag Coverage Δ
lsf-e2e ?
lsf-integration 50.21% <ø> (-0.03%) ⬇️
lsf-unit 8.27% <ø> (ø)
pytests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@luarmr luarmr force-pushed the fb-echo-420/remove-bem-from-dm-lib branch 2 times, most recently from 892f44f to 84a39a1 Compare November 4, 2025 23:00
luarmr added 20 commits November 5, 2025 08:46
Migrated Badge component from Block/Elem to cn() helper.
- Replaced Block import with cn import
- Replaced <Block name="badge-dm"> with <div className={cn("badge-dm")...}>
- Replaced className prop with .mix(className) method
- Preserved all mods, style, and props
- No behavior change, equivalent class strings
Migrated Tag component from Block/Elem to cn() helper.
- Replaced Block import with cn import
- Replaced <Block tag="span" name="tag-dm"> with <span className={cn("tag-dm")...}>
- Preserved mod={{ size }}, mix, style, and all props
- No behavior change, equivalent class strings
Migrated SkeletonGap component from Block/Elem to cn() helper.
- Replaced Elem import with cn import
- Replaced <Elem name="gap"> with <div className={cn("skeletonLoader").elem("gap")...}>
- Used parent Block name "skeletonLoader" from SkeletonLoader component
- Preserved style prop with CSS custom properties
- No behavior change, equivalent class strings
Migrated SkeletonLine component from Block/Elem to cn() helper.
- Replaced Elem import with cn import
- Replaced <Elem name="line"> with <div className={cn("skeletonLoader").elem("line")...}>
- Used parent Block name "skeletonLoader" from SkeletonLoader component
- Preserved style prop with CSS custom properties and key prop
- No behavior change, equivalent class strings
Migrated Counter component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn import
- Replaced <Block name="counter"> with <div className={cn("counter")...}>
- Replaced <Elem tag="input" name="input"> with <input className={cn("counter").elem("input")...}>
- Replaced <Elem name="input"> with <div className={cn("counter").elem("input")...}> (for postfix display)
- Replaced <Elem tag="a" name="btn"> with <a className={cn("counter").elem("btn")...}> in CounterButton
- Preserved all mods (focused, disabled, withPostfix, under, type), refs, handlers, and props
- No behavior change, equivalent class strings
Migrated Form Label component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn and createElement imports
- Replaced <Block tag={tagName} name="label-dm"> with createElement(tagName, {className: cn("label-dm")...})
- Replaced all nested <Elem> components with <div className={cn("label-dm").elem(...)...}>
- Used createElement for dynamic tag handling (div or label based on simple prop)
- Preserved all mods (size, large, flat, placement, withDescription, empty), ref, style, and data-required
- No behavior change, equivalent class strings
Migrated RadioGroup component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn import
- Replaced <Block name="radio-group-dm"> with <div className={cn("radio-group-dm")...}>
- Replaced <Elem name="buttons"> with <div className={cn("radio-group-dm").elem("buttons")...}>
- Replaced <Elem tag="label" name="button"> with <label className={cn("radio-group-dm").elem("button")...}>
- Replaced <Elem tag="input" name="input"> with <input className={cn("radio-group-dm").elem("input")...}>
- Preserved all mods (size, checked, disabled), spread props, and handlers
- No behavior change, equivalent class strings
Completed migration of Form component from Block/Elem to cn() helper.
- Removed Block and Elem imports (cn was already imported)
- Replaced <Block name="form-indicator-dm"> with <div className={cn("form-indicator-dm")...}>
- Replaced <Elem tag="span" name="item"> with <span className={cn("form-indicator-dm").elem("item")...}>
- Most of Form component was already using cn() - only Form.Indicator needed migration
- Preserved mod={{ type: state }} and case prop for Oneof
- No behavior change, equivalent class strings
Migrated Range component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn import
- Replaced <Block name="range"> with <div className={cn("range")...}>
- Replaced all nested <Elem> components with <div className={cn("range").elem(...)...}>
- Updated RangeHandle component to use cn("range").elem("range-handle")
- Updated RangeIndicator component to use cn("range").elem("indicator")
- Preserved all mods (align, with-icon), style props, and event handlers
- No behavior change, equivalent class strings
Migrated Pagination component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn import
- Replaced <Block name="pagination-dm"> with <div className={cn("pagination-dm")...}>
- Replaced all nested <Elem> components with <div className={cn("pagination-dm").elem(...)...}>
- Updated NavigationButton component to use cn("pagination-dm").elem("btn")
- Preserved all mods (disabled, size, waiting, arrow variants), event handlers, and props
- No behavior change, equivalent class strings
Migrated Resizer component from Block/Elem to cn() helper.
- Converted Block name="resizer" to cn("resizer")
- Converted Elem name="content" to cn("resizer").elem("content")
- Converted Elem name="handle" to cn("resizer").elem("handle")
- Preserved all mods (resizing, quickview), refs, handlers, and styles
- No behavior change, equivalent class strings
Migrated FieldsButton component from Elem to cn() helper.
- Converted Elem name="field-button" to cn("field-button")
- Preserved rawClassName by concatenating with toClassName()
- Maintained all styles and tooltip functionality
- No behavior change, equivalent class strings
Migrated MediaPlayer component from Block/Elem to cn() helper.
- Converted Block name="player" to cn("player")
- Converted all Elem components to cn("player").elem()
- Replaced tag props with createElement for dynamic media elements
- Preserved all mods (video), refs, handlers, and click events
- Maintained Space component integration with className prop
- No behavior change, equivalent class strings
Migrated MediaSeeker component from Block/Elem to cn() helper.
- Converted Block name="audio-seeker" to cn("audio-seeker")
- Converted Elem name="wrapper" to cn("audio-seeker").elem("wrapper")
- Converted Elem name="progress" to cn("audio-seeker").elem("progress")
- Converted Elem name="buffer" to cn("audio-seeker").elem("buffer")
- Preserved all mods (video), refs, handlers, and styles
- No behavior change, equivalent class strings
Migrated SkeletonLoader component from Block to cn() helper.
- Converted Block name="skeletonLoader" to cn("skeletonLoader")
- Preserved all styles and CSS variable assignments
- No behavior change, equivalent class strings
Migrated Table DataView component from Block/Elem to cn() helper.
- Converted Block name="fill-container" to cn("fill-container")
- Converted Block name="syncInProgress" to cn("syncInProgress")
- Converted Elem name="title" to cn("syncInProgress").elem("title")
- Converted Elem name="text" to cn("syncInProgress").elem("text")
- Converted Block name="no-results" to cn("no-results")
- Converted Block name="data-view-dm" to cn("data-view-dm")
- Preserved all styles, className mixes, and handlers
- No behavior change, equivalent class strings
Migrated GridView component from Block/Elem to cn() helper.
- Converted Block name="grid-view" to cn("grid-view")
- Converted Elem name="cell-header" to cn("grid-view").elem("cell-header")
- Converted Elem name="cell" to cn("grid-view").elem("cell")
- Converted Elem name="cell-content" to cn("grid-view").elem("cell-content")
- Converted Elem name="cell-body" to cn("grid-view").elem("cell-body")
- Converted Elem name="resize" to cn("grid-view").elem("resize")
- Converted Elem name="list" to cn("grid-view").elem("list")
- Replaced tag props with direct className on AutoSizer and FixedSizeGrid
- Preserved all mods, rawClassName concatenation, refs, and handlers
- No behavior change, equivalent class strings
Migrated Toolbar component from Block to cn() helper.
- Converted Block name="tab-panel" to cn("tab-panel")
- Preserved all Space components and mapped instrument rendering
- No behavior change, equivalent class strings
Migrated ActionsButton component from Block/Elem to cn() helper.
- Converted Block name="dialog-content" to cn("dialog-content")
- Converted Elem name="text" to cn("dialog-content").elem("text")
- Converted Elem name="loading" to cn("dialog-content").elem("loading")
- Converted Elem name="form" to cn("dialog-content").elem("form")
- Converted Block name="actionButton" to Menu.Item with cn("actionButton") className
- Converted Elem name="titleContainer" to cn("actionButton").elem("titleContainer")
- Converted Elem name="title" to cn("actionButton").elem("title")
- Converted Elem name="icon" to IconChevronRight with cn("actionButton").elem("icon") className
- Converted Block name="actionButton-submenu" to cn("actionButton-submenu")
- Preserved all mods, refs, handlers, aria labels, and tooltips
- No behavior change, equivalent class strings
Migrated instruments file from Block to cn() helper.
- Converted Block name="button-wrapper" to cn("button-wrapper")
- Preserved all tooltip configuration and ImportButton wrapper
- No behavior change, equivalent class strings
Migrated Filters component from Block/Elem to cn() helper.
- Converted Block name="filters" to cn("filters")
- Converted Elem name="list" to cn("filters").elem("list")
- Converted Elem name="empty" to cn("filters").elem("empty")
- Converted Elem name="actions" to cn("filters").elem("actions")
- Converted dropdownClassName to use cn("filters").elem("selector").toClassName()
- Preserved all mods (sidebar, withFilters), handlers, and button components
- No behavior change, equivalent class strings
Migrated FiltersSidebar component from Block/Elem to cn() helper.
- Converted Block name="filters-sidebar" to cn("filters-sidebar")
- Converted Elem name="header" to cn("filters-sidebar").elem("header")
- Converted Elem name="extra" to cn("filters-sidebar").elem("extra")
- Converted Elem name="title" to cn("filters-sidebar").elem("title")
- Preserved all button handlers, aria labels, and tooltips
- No behavior change, equivalent class strings
Migrated ImageDataGroup component from Block to cn() helper.
- Converted Block name="grid-image-wrapper" to cn("grid-image-wrapper")
- Preserved all image styles, alt attributes, and AnnotationPreview integration
- No behavior change, equivalent class strings
Migrated Label component from Block/Elem to cn() helper.
- Replaced Block/Elem imports with cn import
- Replaced <Block name="label-view"> with <div className={cn("label-view")...}>
- Replaced all nested <Elem> components with appropriate elements using cn("label-view").elem(...)
- Replaced <Elem tag={Resizer}> with <Resizer className={...}> (component tag pattern)
- Preserved all mods (loading, labelStream, mode, animated), refs, ids, keys, and handlers
- No behavior change, equivalent class strings
Updated GridView test file to use cn() helper instead of Block.
- Converted Block import to cn import
- Updated renderWithBEM wrapper to use cn("grid-view")
- Ensures test mocks match the migrated production code
- No behavior change in tests, equivalent class strings
Removed unused Block/Elem exports from bem.tsx.
- Removed Block, Elem, BemWithSpecificContext, BlockContext, useBEM, and BemComponent exports
- Kept cn, CN, and CNTagName exports (only what's needed)
- All 25 files now use cn() instead of Block/Elem
- Completes DataManager BEM migration
@luarmr luarmr force-pushed the fb-echo-420/remove-bem-from-dm-lib branch from 84a39a1 to 41ea23f Compare November 5, 2025 16:46
@robot-ci-heartex robot-ci-heartex merged commit 059ae27 into develop Nov 5, 2025
45 of 46 checks passed
@robot-ci-heartex robot-ci-heartex deleted the fb-echo-420/remove-bem-from-dm-lib branch November 5, 2025 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants