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:
| Barrel | Contents |
|---|---|
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 |
modules | Composed 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, states | Shared hooks and Jotai atoms (e.g. useConfirm, toast state) |
types | IUIConfig, TPropertyInputProps, search-provider types |
constants, lib, utils | Env constants (NODE_ENV, REACT_APP_API_URL), helpers |
ui-modules (index.ts) exports business-domain modules plus hooks and states:
header:PageHeaderwithStart/End/FavoriteToggleButtoncompositioncontacts,products,sales,segments,tags,properties,permissions,structure,team-members,brandsautomations,approval,documents,import-export,internal-notes,activity-logs,notifications,payments,templates,widget,favorites,shared,tags-new- Plugin-facing atoms such as
pluginsConfigState(the loadedIUIConfigmap)
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 })plususeRecordTableanduseRecordTableCursorfor cursor-paginated lists.Form.Field: React Hook Form field wrapper (withForm.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).filtermodule: 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 generatedIndexPagealready composes it withBreadcrumbandSeparator.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__andsrc/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 (aSelectMember-style module may already exist undercontacts,team-members, orsegments).
Rules
- No direct
@radix-ui/*imports and no hand-rolled primitives:erxes-uicomponents 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_uideals,operation_uitasks,frontline_uiinbox) and reuse its pattern without importing its source. - Import only through the package names (
erxes-ui,ui-modules); never deep-importfrontend/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.