Shared UI Components

Reuse the erxes-ui primitives and ui-modules business modules instead of building parallel components.

Both libraries are Module Federation singletons; the host shares one copy with every remote. For how remotes load, see Frontend Plugins.

What each library exports

erxes-ui (index.ts) barrels seven areas:

BarrelContents
components~60 primitives: Button, Form, Sheet, Dialog, Select, Combobox, Table, Tabs, Sidebar, CommandBar, Command, Breadcrumb, Tooltip, Spinner, Skeleton, Empty, Toast/Toaster, Dropzone, date/time pickers, and more
modulesComposed feature blocks: RecordTable, board, filter, record-field, inputs, select-tree, select-operation, navigation-menu, side-menu, focus-sheet, attachments, preview, report, display, layout, icons, blocks, hotkey
hooks, state, statesShared hooks and Jotai atoms (e.g. useConfirm, toast state)
typesIUIConfig, TPropertyInputProps, search-provider types
constants, lib, utilsEnv constants (NODE_ENV, REACT_APP_API_URL), helpers

ui-modules (index.ts) exports business-domain modules plus hooks and states:

  • header: PageHeader with Start/End/FavoriteToggleButton composition
  • contacts, products, sales, segments, tags, properties, permissions, structure, team-members, brands
  • automations, approval, documents, import-export, internal-notes, activity-logs, notifications, payments, templates, widget, favorites, shared, tags-new
  • Plugin-facing atoms such as pluginsConfigState (the loaded IUIConfig map)
import { Button, Form, RecordTable, Sheet } from 'erxes-ui';
import { PageHeader } from 'ui-modules';

Composed components to reach for first

Prefer composed components over hand-rolled layouts:

  • RecordTable (record-table): Object.assign(RecordTableRoot, { Provider, Header, Head, InlineHead, RowList, Body, Cell, Row, RowSkeleton, MoreButton, checkboxColumn, Scroll, CursorProvider, CursorBackwardSkeleton, CursorForwardSkeleton, ColumnSelector }) plus useRecordTable and useRecordTableCursor for cursor-paginated lists.
  • Form.Field: React Hook Form field wrapper (with Form.useField) for label/control/message per field.
  • Sheet.Content: the side panel used for detail/edit flows.
  • CommandBar: the bulk-action bar for selected table rows (Object.assign(Root, { ... }) composition).
  • filter module: query-state-driven filter bars and date filters.
  • NavigationMenuLinkItem / SettingsNavigationMenuLinkItem: the generated navigation uses these for plugin sidebar entries.
  • PageHeader (ui-modules): standard page chrome; the generated IndexPage already composes it with Breadcrumb and Separator.Inline.
  • select-operation, select-tree: member/company/segment-style selectors.

Icons come from @tabler/icons-react (e.g. IconSandbox, IconBriefcase); erxes-ui also includes IconsProvider, IconComponent, and IconPicker for user-chosen icons.

A full composition

sales_ui's PipelineRecordTable shows the intended stack in one file: table state from URL query state, confirmation on destructive actions, translations via the plugin namespace:

import { RecordTable, RecordTableTree } from 'erxes-ui';

<RecordTable.Provider
  columns={pipelinesColumns(t)}
  data={pipelines || []}
  stickyColumns={['more', 'checkbox', 'name']}
>
  <PipelineCommandBar />
  <RecordTableTree id="pipelines-list" ordered>
    <RecordTable.Scroll>
      <RecordTable>
        <RecordTable.Header />
        <RecordTable.Body>
          <RecordTable.RowList Row={RecordTableTree.Row} />
          {loading && <RecordTable.RowSkeleton rows={30} />}
          {!loading && pageInfo?.hasNextPage && (
            <RecordTable.RowSkeleton rows={1} handleInView={handleFetchMore} />
          )}
        </RecordTable.Body>
      </RecordTable>
    </RecordTable.Scroll>
  </RecordTableTree>
</RecordTable.Provider>

Cursor-style infinite scroll is a skeleton row whose handleInView calls the page's fetch-more handler; the same file uses useConfirm for destructive actions and useTranslation('sales') for the plugin's i18n namespace.

Find the pattern before you build

  • rg "RecordTable.Provider" frontend/plugins --files-with-matches: find every plugin already using a component, then read the closest one.
  • frontend/libs/erxes-ui/src/components/__stories__ and src/modules/__stories__: per-component Storybook stories (button.stories.tsx, commandbar.stories.tsx, form.stories.tsx) show intended usage.
  • rg "<ComponentName>" frontend/libs/ui-modules/src: find the shared wrapper before composing your own (a SelectMember-style module may already exist under contacts, team-members, or segments).

Rules

  • No direct @radix-ui/* imports and no hand-rolled primitives: erxes-ui components are already the Radix layer; the host style system assumes them.
  • Search before building: find a similar implementation before writing new code. Before a new list, form, or detail view, open a plugin that already has one (sales_ui deals, operation_ui tasks, frontline_ui inbox) and reuse its pattern without importing its source.
  • Import only through the package names (erxes-ui, ui-modules); never deep-import frontend/libs/*/src/... paths, and never import another plugin's files.
  • Keep plugin-specific components, hooks, translations, and utilities inside frontend/plugins/<name>_ui/src/ (aliases ~/* → src, @/* → src/modules).
  • Match nearby spacing, loading, empty, and error patterns; avoid inline styles unless the neighboring code uses them for the same pattern.
Was this helpful?