salesforce-lwc-production-toolkit
Reusable, production-grade Lightning Web Components and Apex services for the pieces every Salesforce project ends up rebuilding: a generic server-side datatable, a modal framework, error/state handling, and the small utility functions that don't deserve a bespoke implementation per project.
This is not a lightning-button wrapper library. Everything here either does something non-trivial (server-side pagination + partial-error reporting) or removes genuine, repeated boilerplate (Apex/LDS error parsing, debounce, nested-path access).
Why
Every Salesforce project eventually builds some version of a server-side, sortable, filterable, editable datatable with bulk actions - and re-derives the same Apex error parsing, the same modal boilerplate, the same loading/empty/error branch. This package is that "some version," built once, tested, and documented, so the question stops being "why are we rebuilding this again?"
Requirements
- Salesforce CLI (
sf), authenticated to a Dev Hub or sandbox - API 61.0+ org (Summer '24+) - the toolkit uses
AccessLevel.USER_MODE,Security.stripInaccessible,Database.queryWithBinds/countQueryWithBinds, andlightning/modal, all of which need a reasonably current org - Node.js 18+ (for linting/Jest only - not required to deploy)
Setup
sf org create scratch -f config/project-scratch-def.json -a lwc-toolkit -d 7 sf project deploy start -o lwc-toolkit sf org assign permset -o lwc-toolkit -n LWCToolkitAdmin sf org open -o lwc-toolkit
LWCToolkitAdmin grants access to the four Apex services, the Toolkit_Bulk_Edit custom permission, and read access to the ToolkitDemoSetting__mdt custom metadata fields. Assign it to any user who should see the demo data load correctly.
Then drop Enterprise Datatable Demo (Accounts) onto an App Page, Record Page, or Home Page from Lightning App Builder to see the whole pipeline running against Account, or open enterpriseDatatableDemo for the wiring pattern to copy for your own object.
Local development (linting/tests only)
npm install npm test # Jest unit tests for every LWC/util module npm run lint # ESLint npm run prettier # format everything
190 Jest tests and 24 Apex test methods cover the components and services below.
Architecture
force-app/main/default/
├── classes/ Generic Apex services (query, DML, permissions, custom metadata, files)
├── customMetadata/ Demo config record consumed by metadataUtils
├── customPermissions/ Toolkit_Bulk_Edit, gating bulk edit/delete in the demo
├── messageChannels/ ToolkitMessageChannel, backing lmsUtils
├── objects/ ToolkitDemoSetting__mdt (the demo's configuration type)
├── permissionsets/ LWCToolkitAdmin
└── lwc/
├── enterpriseDatatable/ the flagship component
├── enterpriseDatatableDemo/ ready-to-drop Account example
├── multiSelectLookup, dynamicFilterPanel, dynamicForm, pagination,
│ fileUploadManager, imageGallery other data components
├── modal, confirmDialog, wizard, stateManager, emptyState, errorState,
│ bulkActionBar, infiniteScrollLoader, permissionGate, featureGate,
│ kpiCard, dateRangePicker UX components
└── functionUtils, objectUtils, jsonUtils, urlUtils, apexErrorUtils,
ldsErrorUtils, navigationUtils, recordUtils, permissionUtils,
metadataUtils, toastService, lmsUtils data/Salesforce utility modules
The generic Apex layer
Rather than a bespoke @AuraEnabled controller per object, four schema-driven services back everything:
GenericQueryService.query(...)- dynamic SOQL: object/field/filter identifiers are validated against the running user's schema describe (never concatenated as literals), values are always bound viaDatabase.queryWithBinds/countQueryWithBinds, and the query itself runs underAccessLevel.USER_MODEas a second layer of CRUD/FLS enforcement. Supports paging, sorting, per-field filters, and a multi-fieldLIKEsearch.GenericDmlService.saveRecords(...)/.deleteRecords(...)- bulk insert/update/delete withallOrNone=false,Security.stripInaccessiblefor FLS, and aDmlResultper row (correlated byrowKey) so a batch save reports exactly which rows failed instead of aborting the whole batch.PermissionService- object/field access checks and a custom-permission check (viaSetupEntityAccess/PermissionSetAssignment, since Apex has no direct "does this user have this custom permission" API).CustomMetadataService- generic Custom Metadata Type reader, validated to only ever query__mdtobjects.
The flagship: c-enterprise-datatable
Server-side query → Pagination → Filtering → Sorting → Inline editing → Bulk update → Partial errors → Refresh
Point it at an object and a (mostly standard lightning-datatable) column array:
<c-enterprise-datatable object-api-name="Account" title="Accounts" columns={columns} page-size="25" pagination-mode="pages" search-fields={searchFields} bulk-actions={bulkActions} enable-row-selection enable-bulk-delete default-sort-field="Name" onbulkaction={handleBulkAction} ></c-enterprise-datatable>
columns = [ { label: 'Name', fieldName: 'Name', type: 'text', editable: true, filterable: true }, { label: 'Owner', fieldName: 'Owner.Name', type: 'text' } // relationship fields are auto-flattened ];
What it does with that config:
- Server-side query via
GenericQueryService, fields derived automatically from yourcolumns. - Pagination:
pagination-mode="pages"(Previous/Next + a page indicator) or"infinite"(ac-infinite-scroll-loadersentinel at the bottom, IntersectionObserver-based). - Filtering: any column with
filterable: truegets a quick-filter input;filterOperator(default'=') controls the SOQL operator per column. - Sorting: native
lightning-datatablecolumn-header sort, re-queried server-side. - Multi-select + bulk actions:
enable-row-selectionturns on the checkbox column; selected rows drivec-bulk-action-bar. Pass your ownbulk-actions(re-emitted as abulkactionevent for you to handle) and/or setenable-bulk-deletefor a built-in destructive action, gated onPermissionService.checkObjectAccess(...).isDeletableand ac-confirm-dialogconfirmation. - Inline editing → bulk update → partial errors: edited rows save through
GenericDmlService.saveRecordsin one batch; failed rows stay indraft-valuesand are marked vialightning-datatable'serrorsprop with the server's message, while successful rows commit and fire a success toast - a save of 20 rows where 2 fail doesn't lose your other 18 edits. - Refresh: a header button, plus a public
@api refresh()for a parent (or a Lightning quick action) to call. - Relationship-path columns (
fieldName: 'Owner.Name') are queried as-is and flattened onto an alias key, sincelightning-datatablerequires flat, dot-free field names.
c-enterprise-datatable-demo wires all of this against Account, including reading the default page size from ToolkitDemoSetting__mdt via metadataUtils and gating its bulk-delete action on the Toolkit_Bulk_Edit custom permission via permissionUtils - copy that component as the starting point for your own object.
Feature map
Data components
| Requested | Where |
|---|---|
| Generic server-side datatable | enterpriseDatatable + GenericQueryService |
| Infinite scrolling | infiniteScrollLoader (standalone) + enterpriseDatatable's pagination-mode="infinite" |
| Server-side pagination | enterpriseDatatable's pagination-mode="pages", GenericQueryService offset/limit |
| Sorting/filtering | enterpriseDatatable column sort + per-column filterable quick filters |
| Multi-select | enterpriseDatatable's enable-row-selection (native lightning-datatable checkboxes) |
| Inline editing | enterpriseDatatable's draft-values/onsave wired to GenericDmlService |
| Bulk actions | bulkActionBar (standalone) + enterpriseDatatable's bulk-actions/enable-bulk-delete |
| Advanced data table | enterpriseDatatable above — this is that component, not a second one |
| Multi-select lookup | multiSelectLookup - type-to-search against any object via GenericQueryService |
| Dynamic filter panel | dynamicFilterPanel - builds QueryFilter[] for GenericQueryService/enterpriseDatatable |
| Dynamic form | dynamicForm - renders from a field-config array, field-level-access-aware |
| Pagination / keyset pagination | pagination (standalone) + GenericQueryService.queryWithCursor |
| File upload manager | fileUploadManager + FileManagerService.cls |
| Image gallery | imageGallery - thumbnail grid + c-modal lightbox |
UX components
| Requested | Where |
|---|---|
| Modal framework | modal - slot-based, focus-trapped, works anywhere (Experience Cloud/LWR included) |
| Confirmation dialog | confirmDialog - promise-based, built on lightning/modal |
| Toast manager | toastService - consistent ShowToastEvent dispatch with de-duplication |
| Loading/state manager | stateManager |
| Empty-state component | emptyState |
| Error-state component | errorState |
| Wizard/stepper | wizard - lightning-progress-indicator + cancelable beforestepchange for per-step validation |
| Confirmation modal | confirmDialog above |
| Error panel | errorState above |
| Permission-aware component | permissionGate - gates slotted content on object/field/custom-permission checks |
| Metadata-driven configuration provider | featureGate - feature-flag gating driven by any Custom Metadata Type |
| KPI dashboard card | kpiCard |
| Date range picker | dateRangePicker - with quick-range presets |
Data utilities
| Requested | Where |
|---|---|
| Debounce/throttle | functionUtils |
| Deep clone | objectUtils.deepClone |
| Object comparison | objectUtils.isEqual |
| Nested property access | objectUtils.getNestedValue/setNestedValue |
| JSON formatting | jsonUtils |
| URL/query parameter helpers | urlUtils |
Salesforce utilities
| Requested | Where |
|---|---|
| Apex error parser | apexErrorUtils |
| LDS error parser | ldsErrorUtils |
| Navigation helpers | navigationUtils |
| Record refresh helpers | recordUtils - pairs refreshApex with notifyRecordUpdateAvailable in one call |
| Permission checking | permissionUtils + PermissionService.cls |
| Custom metadata configuration loader | metadataUtils + CustomMetadataService.cls |
| LMS (Lightning Message Service) wrapper | lmsUtils + ToolkitMessageChannel |
Known limitations / extension points
- The App Builder property panel only exposes
enterpriseDatatable's primitive properties (object, title, page size, ...) -columns,staticFilters,bulkActions, andsearchFieldsare arrays/objects and must be set by wrapping the component in your own LWC (asenterpriseDatatableDemodoes), not by an admin in Lightning App Builder. GenericDmlServicesupports insert/update/delete; it does not run Apex triggers differently than any other DML, so validation rules/triggers on the target object still apply and their errors surface through the normal partial-error path.- Custom Metadata field-level filtering in
metadataUtils/CustomMetadataServicereturns full records (no server-side filtering by field value) - it's a configuration loader, not a query engine; useGenericQueryServiceif you need to query a__mdttype with filters. dynamicForm'spicklistfield type doesn't callgetPicklistValuesByRecordTypefor you - passoptionsexplicitly, since record-type-aware picklists are a meaningfully bigger feature than this component takes on.GenericQueryService.queryWithCursor(keyset pagination) is always sortedId ASC- it doesn't support arbitrary-field sort with tie-breaking, which is a substantially more complex feature; usequery()(offset pagination) when you need to sort by something else.