Release Notes & Upgrade Guide
All packages release together from a single repository: one release commit bumps every packages/*/package.json, and the release is tagged vX.Y.Z. The bump is applied per package relative to its own manifest (the release script runs pnpm version in each package), so all packages move by the same increment — but a package added mid-cycle starts from whatever version its scaffold declared, and its number can trail the tag until the repository's sync-meta maintenance script realigns it with the root. This is not theoretical: the v2.12.0 release published @vef-framework-react/integration as 2.10.0 while every other package published as 2.12.0.
The upgrade rule is therefore: upgrade every @vef-framework-react/* dependency together, each to the version published by the same release — Installation pins the current set. Mixing packages from different releases in one app is not supported.
The package set itself can change between releases: v2.7.0 removed @vef-framework-react/expression, v2.8.0 added @vef-framework-react/approval-form-bridge, v2.12.0 added @vef-framework-react/approval and @vef-framework-react/integration, and the next release adds @vef-framework-react/cron — all are called out below.
Versions are listed newest-first, starting with changes that are merged but not yet released. Each version leads with breaking changes, then features, then fixes worth knowing. Changes to the internal playground demo and repository tooling are omitted.
Unreleased (after v2.12.0, as of 2026-07-20)
Merged on the main branch but not yet in a tagged release. If you consume tagged releases only, these changes arrive with the next version.
Breaking: "dictionary" is renamed to "code set" everywhere
The data-dictionary vocabulary is renamed to code set across all packages. The wire contract is untouched — the host still supplies one query function that maps key strings to DataOption[] — but every exported name containing "dictionary" changes. See Code Sets for the concept guide.
hooks — the query hook and its whole type family:
// Before (≤ v2.12.0)
import {
resolveDictKey,
useDictionaryQuery,
type DictionaryAliasMap,
type DictionaryKey,
type DictionaryKeyConfig,
type DictionaryKeyValue,
type DictionaryQueryData,
type UseDictionaryQueryOptions
} from "@vef-framework-react/hooks";
// After
import {
resolveCodeSetKey,
useCodeSetQuery,
type CodeSetAliasMap,
type CodeSetKey,
type CodeSetKeyConfig,
type CodeSetKeyValue,
type CodeSetQueryData,
type UseCodeSetQueryOptions
} from "@vef-framework-react/hooks";
The Register augmentation member follows — projects that constrain keys must update their declare module block:
declare module "@vef-framework-react/hooks" {
interface Register {
// Before: dictionaryKeys: "sys.menu.type" | "sys.user.gender";
codeSetKeys: "sys.menu.type" | "sys.user.gender";
}
}
core — AppContext.dictionaryQueryFn is now AppContext.codeSetQueryFn. Update the appContext passed to createApp().render():
createApp().render({
// ...
appContext: {
hasPermission,
// Before: dictionaryQueryFn: findCodeSetEntries,
codeSetQueryFn: findCodeSetEntries,
fileBaseUrl: "/files"
}
});
components — useDictionaryOptionsSelect is now useCodeSetOptionsSelect; its option/result types UseDictionaryOptionsSelectOptions / UseDictionaryOptionsSelectResult are now UseCodeSetOptionsSelectOptions / UseCodeSetOptionsSelectResult.
dev — the code-generation module renames in lockstep (see Dev Package):
- CLI command:
vef gen:dictionary-keys→vef gen:code-set-keys. code-generation.config.ts: thedictionaryKeysblock →codeSetKeys, itsfetchDictionaryKeysfetcher →fetchCodeSetKeys.- Default output file:
src/types/dictionary.gen.ts→src/types/code-set-keys.gen.ts; the generated union type isCodeSetKey(wasDictionaryKey) and augmentsRegister.codeSetKeys. - Programmatic exports:
generateDictionaryKeys→generateCodeSetKeys(with itsOptions/Resulttypes),renderDictionaryKeysFile→renderCodeSetKeysFile(withRenderCodeSetKeysOptions),DICTIONARY_AUGMENT_TARGET→CODE_SET_AUGMENT_TARGET,DictionaryKeyEntry→CodeSetKeyEntry,DictionaryKeysConfig→CodeSetKeysConfig.
After renaming the config, delete the old generated file and rerun vef gen:code-set-keys so the augmentation targets the new codeSetKeys member.
Breaking: approval-flow-editor returns to a hand-written state engine
The editor's state engine is hand-written again and the @coldsmirk/nodeloom-core dependency is gone. This reverses the in-memory node shape that v2.7.0 introduced (see that section below); the wire format is once again untouched.
// v2.7.0 – v2.12.0 (nodeloom shapes): kind at data.kind, business fields under data.config
import type { FlowNode } from "@vef-framework-react/approval-flow-editor";
const isUrgent = (node: FlowNode) =>
node.data.kind === "approval" && node.data.config.name === "Urgent";
// After (typed xyflow nodes): kind at node.type, business fields directly on data
import type { FlowNode } from "@vef-framework-react/approval-flow-editor";
const isUrgent = (node: FlowNode) =>
node.type === "approval" && node.data.name === "Urgent";
FlowNodeis now a package-local discriminated union of typed@xyflow/reactnodes (Node<StartNodeData, "start"> | … | Node<CcNodeData, "cc">);FlowEdgeis xyflow'sEdge. Neither is a nodeloom re-export anymore.- Per-kind node aliases are exported again — but only
StartNode,ApprovalNode,HandleNode, andCcNode. Unlike pre-v2.7.0,ConditionNodeandEndNodestay internal; their data shapes (ConditionNodeData,EndNodeData) remain exported. AnyNodeDatasurvives as an alias of the node-data union (FlowNode["data"]).- The internal store was restructured into slices; hosts selecting from
useEditorStoremust adapt to the newEditorStateshape (again). - The wire format is untouched.
toFlowDefinition/fromFlowDefinitionand theNodeDefinition(value/onChange) contract are unchanged — the serializer now mapsnode.type↔kind. Saved flow definitions load without migration.
Features
- New package:
@vef-framework-react/cron— drop-in management pages for the durable cron scheduling engine:CronSchedulePageandCronRunPage(plusRunDetailDrawer, form-model helpers, andCRON_PERMISSIONS). Schedule list filters are split into basic and advanced panels; a paused schedule hides its fire cursor. See Engines. Not on npm yet — it first publishes with the next release. - core —
PushClient(and thecreatePushClientfactory) for the vef server push channel: a downstream-only WebSocket that deliversPushMessageenvelopes ({ id, type, payload?, time }).subscribe(type, handler)registers by envelope type ("*"receives everything) and returns the unsubscribe function. Transport losses reconnect with jittered exponential backoff (PushReconnectOptions), re-reading the access token per attempt (sent as the__accessTokenquery parameter); the terminal close codes4401(PUSH_CLOSE_SESSION_INVALID) and4429(PUSH_CLOSE_TOO_MANY_CONNECTIONS) surface throughonSessionInvalid/onConnectionRejectedand never reconnect. Delivery is best-effort by contract — treat it as a real-time hint and keep reliable state behind the regular API. - hooks —
usePushMessage(client, type, handler)subscribes to one envelope type with automatic cleanup on unmount. - integration — code map management page (
IntegrationCodeMapPage) with completions for map codes; code map code sets pick from the host catalog with a manual-entry fallback.
Fixes worth knowing
- components — the pro-search collapse toggler passes the correct antd icon prop (the chevron renders again), and the actions area sizes responsively so search controls can shrink beside toolbar content.
v2.12.0 (2026-07-17)
Two packages join the set: @vef-framework-react/approval (first published as 2.12.0) and @vef-framework-react/integration (first published as 2.10.0 — its manifest was scaffolded mid-cycle; see the versioning note at the top of this page). Pin integration to ^2.10.0 until a later release realigns it.
No migration is needed when upgrading from v2.11.0. The release contains two breaking-flagged commits, but both reshaped the approval package before its first publish: the designer seeding rework (slim version summaries) and the labels-suite move out of approval into components. For released consumers the labels move is purely additive — the suite lands in components as new exports.
New package: @vef-framework-react/approval
Drop-in management and runtime pages for the approval engine, embedding the flow designer end to end: ApprovalFlowPage (with FlowDesignerDrawer / FlowVersionsDrawer), ApprovalTaskCenterPage, ApprovalMyInstancesPage, ApprovalInitiatePage (with StartInstanceDrawer), ApprovalAdminPage, ApprovalCategoryPage, and ApprovalDelegationPage, plus ApprovalProvider / useApprovalPlugins / toEditorPlugins for host-supplied pickers and APPROVAL_PERMISSIONS. See Engines.
Notable behavior shipped in this first release: the designer seeds from the latest deployed version via slim FlowVersionSummary records (the full definition is fetched per version through getGraph with an explicit versionId), flow labels appear on instance detail, the instance detail action bar gains remove-assignee, and metrics load failures surface a retry action.
New package: @vef-framework-react/integration
Drop-in management pages for the integration engine: IntegrationSystemPage, IntegrationAdapterPage, IntegrationContractPage, IntegrationRoutePage, and IntegrationConsolePage, plus INTEGRATION_PERMISSIONS. See Engines.
Notable behavior shipped in this first release: a full-height console workbench with inner scroll areas, a scheme-aware auth params editor with dedicated credential fields, script API completions in every script editor with hoverable script docs rendered from the completion catalogs, contract labels management with filtered directories, and JSON Schema keyword completions with highlighted wire-trace bodies.
Features
- components — the labels suite (moved from the pre-release approval package, so it lands here as new exports):
LabelsEditor,LabelsDisplay,LabelFilterSelect, the helpersformatLabelFilters/parseLabelFilters/isValidLabel, and theLABEL_KEY_PATTERNconstant. - components —
FlexTabsandLabeledlayout primitives for full-height tabbed layouts and consistently labeled values. - components — code editor: a declarative
completionsprop (CompletionEntry[]) adds API completions alongside the language's own (on"json", entries complete as object keys inside property-name strings); the"xml"language joins the built-in list; a floating format action (showFormat, defaulttrue) formats JSON losslessly through prettier. - components —
formLayoutpasses through onFormModal,FormDrawer, andCrud;EditableTableforwardslocaleto the underlying table.
Fixes worth knowing
- core — path-parameter substitution only matches at the start of a path segment and requires the name to begin with a letter or underscore, so the port in an absolute URL (
https://minio.local:9000/…) and mid-segment colons (/schedule/12:30) are never treated as parameters. - components — a stale async format result no longer overwrites newer keystrokes; code-editor tooltips render above antd overlay layers (modals, drawers); the editable-table operation column gets a fixed default width; drawer form content stays clear of the footer.
v2.11.0 (2026-07-15)
Breaking: request lifecycles are isolated per invocation
ApiClient.createQueryFn used to call your factory once, at definition time, and inject the abort signal of "the current query" through shared mutable state — concurrent or interleaved async queries could observe each other's signals. The factory now runs once per query execution, and each handler receives an invocation-scoped HttpClient proxy bound to that execution's abort signal (combined with any explicit options.signal passed to a request method).
The contract change: factories must be side-effect-free, must not retain state across executions, and cannot perform one-time setup. Move any such code out of the factory:
// Before (≤ v2.10.0): the factory body ran once and could hold shared state
const findUserPage = apiClient.createQueryFn("findUserPage", http => {
const columnCache = buildColumnCache(); // one-time setup — no longer safe here
return async (params: UserSearch) => http.post("/api/user/page", { data: params });
});
// After (≥ v2.11.0): one-time setup lives outside; the factory only closes over it
const columnCache = buildColumnCache();
const findUserPage = apiClient.createQueryFn(
"findUserPage",
http => async (params: UserSearch) => http.post("/api/user/page", { data: params })
);
createMutationFn is unchanged — it is still constructed once.
Token refresh coordination was rebuilt on the same principle. On a 401 with a configured tokenExpiredCode, HttpClient publishes one shared refresh promise; concurrent new requests wait through request-scoped, abort-aware waiters and resume with the renewed token. Behavior you can now rely on:
- Canceling a waiting request (or the request whose 401 triggered the refresh) does not cancel the global refresh.
- After a successful refresh, a still-live triggering request retries with the renewed token.
- When an automatic refresh fails,
onUnauthenticatedis called exactly once — even if the triggering request was canceled in the meantime. ensureTokenRefreshed(false)suppresses the unauthenticated callback only when that call owns (started) the refresh cycle; joining an in-flight refresh cannot override the owner's failure policy.
The same release normalizes file responses: a response that carries the backend's JSON business envelope where a file was expected surfaces as a BusinessError instead of being returned as file content, and raw (non-envelope) download responses are handled correctly.
Features
- core —
HttpClient.requestFile(url, options?)fetches a file as aBlobwith the full request semantics of the client (Bearer injection, 401 refresh-and-retry, path parameters, abort signal) and returnsHttpFileResponse—{ blob: Blob; filename?: string }, the filename parsed fromContent-Disposition.download()is now built on top of it and keeps its trigger-a-browser-download behavior.requestFileparticipates increateQueryFnsignal injection like the other request methods. - components — the file-preview contract: the framework bundles no viewer library; instead
FilePreviewProviderinstalls an app-level preview host (aFilePreviewHandler), andUpload— plus everything built on it — dispatches non-image files to it as normalizedFilePreviewTargets (toFilePreviewTarget, reading theUploadedFileMetastamped on upload/hydrate). Images keep the built-inImagemodal. Behavior change alongside it: without an accepting provider the framework warns and no longer hands the browser a URL whose authentication requirements it cannot prove. Private files are fetched by the host throughHttpClient.requestFile/HttpClient.download. The playground demonstrates a complete host built on@file-viewer/react.
Fixes worth knowing
- components — authenticated upload previews resolve their source URLs in isolation, per file, instead of sharing state across items.
v2.10.0 (2026-07-13)
Breaking: AuthTokens.refreshToken is optional
Backends that issue stateful opaque-token sessions (server-side sliding expiration — no refresh round-trip) return no refresh token; only JWT-mode backends do. The type now reflects that:
// Before (≤ v2.9.0)
export interface AuthTokens {
accessToken: string;
refreshToken: string;
}
// After (≥ v2.10.0)
export interface AuthTokens {
accessToken: string;
refreshToken?: string;
}
Strictly a compile-time migration: any code that reads tokens.refreshToken into a string-typed slot — most commonly the refreshToken callback passed to createApiClient — must now handle undefined:
async refreshToken(tokens) {
if (!tokens.refreshToken) {
// Opaque-token session: nothing to refresh; let the request fail as expired.
throw new Error("No refresh token");
}
// ... exchange tokens.refreshToken as before
}
Hosts on opaque-token backends can simply omit the refreshToken callback — the 401 path then goes straight to onUnauthenticated.
Breaking: form-editor and approval-form-bridge contract tightening
Two editor-stack breaking changes ship in this release; the details live in Visual Editors:
- form-editor — every programmatic write is gated behind the field-permission clamp, so a write to a field the current permissions mark read-only or hidden no longer lands.
- approval-form-bridge — the projection emits a bare field array (
ProjectionResult.fields: ApprovalFormField[]replaces thedefinitionwrapper object), and the rich form schema is sent at deploy time. TheApprovalFormDefinitiontype was removed with no replacement — see the migration note.
Features
- starter — built-in
password_changechallenge renderer for the backend's forced password-change login challenge: thePasswordChangeChallengecomponent, thePASSWORD_CHANGE_CHALLENGE_TYPEconstant ("password_change"), and thePasswordChangeChallengeSpec/PasswordChangeChallengeData/PasswordChangeChallengeProps/PasswordChangeReasontypes. Register it inchallengeRenderersand augmentRegister['challenges']— see Application Shell. - approval-flow-editor — tri-state
formFieldsinventory and a required auto-pass mirror in the designer; deploy-time field-permission validation is mirrored at design time. - form-editor — runtime field state is clamped with server-resolved
fieldPermissions; stack blocks gain width/min/max/align sizing; the designer scrolls the selection into view, adds a container drag chip, and supports option reordering. - approval-form-bridge — cross-device classification stays in sync, and hidden required fields produce a warning at projection time.
Fixes worth knowing
- components — pro-search fields keep their width when an input uses
allowClear. - dev —
VEF_APP_NAMEnow takes effect for the injected config global.defineViteConfigpreviously loaded onlyVEF_BUILD_*variables fromenv/, so the name never reached the config plugin; additionally, the production__VEF_APP_CONFIG__define and the generatedapp.config.jsderived the global's name differently and could disagree. Both sides now resolve the same__PRODUCTION__VEF_<CONSTANT_CASE_NAME>__CONF__global. See Dev Package.
v2.9.0 (2026-07-06)
No breaking changes.
Features
- starter — the app-switcher popover now fits four apps per row.
v2.8.0 (2026-07-06)
No breaking changes.
Features
- New package:
@vef-framework-react/approval-form-bridge— a pure-function projection bridge betweenform-editorschemas and the approval backend's flat form contract. It exportsprojectFormSchema,createApprovalRegistries,validateApprovalSchema, andAPPROVAL_EXCLUDED_FIELD_TYPES. Add it to your dependencies at the same version as the other packages. See Approval Form Bridge. - approval-flow-editor — the designer now mirrors the backend's aggregate and branch-condition validation, including detail-table aggregate conditions, so contract violations surface at design time instead of at deploy.
Fixes worth knowing
- approval-flow-editor — parallel add-assignee is excluded from sequential-approval defaults, matching what the backend accepts.
v2.7.0 (2026-07-05)
The largest release in this range: the form-editor expression engine and the approval-flow-editor state engine were both replaced. If you persist form schemas with linkage expressions, audit them before upgrading.
Breaking: @vef-framework-react/expression removed
The ZEN (GoRules) expression runtime package was deleted outright. Remove it from your dependencies. The only surviving public surface, the condition-operator vocabulary, moved to approval-flow-editor:
// Before (≤ v2.6.0)
import { CONDITION_OPERATORS, type ConditionOperator } from "@vef-framework-react/expression";
// After (≥ v2.7.0)
import { CONDITION_OPERATORS, type ConditionOperator } from "@vef-framework-react/approval-flow-editor";
Everything else — evaluate / evaluateSync, loadEngine / configureEngine, ExpressionError — is gone with no replacement.
Breaking: form-editor linkage expressions are now plain JavaScript
Linkage expression conditions and expression-valued assignments used to run through the shared ZEN evaluator; they are now plain JavaScript compiled with new Function (script actions always were). This changes the language of stored schemas: any persisted expression written in ZEN dialect must be rewritten as a JavaScript expression.
// Before (ZEN dialect)
field.age >= 18 and field.status == "active"
// After (JavaScript)
$form.age >= 18 && $form.status === "active"
- The evaluation scope is unchanged:
field/$form(form values —fieldis the legacy alias),$vars,$user,$node,$now. An expression that is valid in both dialects (e.g.$form.age >= 18) keeps working as-is. - ZEN-only syntax (
and/or/not, ZEN built-in functions) no longer parses; a malformed source degrades tofalsefor conditions andundefinedfor assignment values rather than throwing. - The type
ExpressionContextwas renamed toEvaluationContext; theLinkageEvaluatorscallback signatures now takeEvaluationContext. - CSP: the default evaluators now require
'unsafe-eval'for all expression kinds, where previously only script actions did. Supply your ownLinkageEvaluatorsto swap in a sandboxed engine.
Alongside the engine swap, FormEditor gained host context injection: an evaluationContext prop carrying the live $vars / $user / $node values, and a contextSources prop (LinkageContextSource[]) that publishes host-declared paths such as "$user.departmentId" to the visual condition builder's source picker. Declaring a source only affects the pick list — evaluation reads evaluationContext either way.
Breaking: approval-flow-editor state engine rebuilt on nodeloom
The editor's state engine was rebuilt on @coldsmirk/nodeloom-core. In-memory node and edge shapes are now nodeloom's uniform ones: every node carries type: "flowNode", the kind discriminator lives at data.kind, and the business fields sit under data.config.
// Before (≤ v2.6.0): per-kind xyflow node types
import type { ApprovalNode } from "@vef-framework-react/approval-flow-editor";
const isUrgent = (node: ApprovalNode) =>
node.type === "approval" && node.data.name === "Urgent";
// After (≥ v2.7.0): uniform FlowNode, discriminated by data.kind
import type { FlowNode } from "@vef-framework-react/approval-flow-editor";
const isUrgent = (node: FlowNode) =>
node.data.kind === "approval" && node.data.config.name === "Urgent";
- The per-kind type aliases
StartNode,EndNode,ApprovalNode,HandleNode,ConditionNode, andCcNodeare no longer exported;FlowNode/FlowEdgeare re-exports of nodeloom's types.AnyNodeDataremains exported. - The internal store was restructured; hosts selecting from
useEditorStoremust adapt to the newEditorStateshape. - The wire format is untouched.
toFlowDefinition/fromFlowDefinitionand theNodeDefinition(value/onChange) contract are unchanged, so saved flow definitions load without migration.
Features
- approval-flow-editor — new
plugins.globalSubjects(FormFieldDefinition[]) injects host-declared subjects into the condition builder alongside the built-in applicant attributes andformFields. See Host Integration. - starter — the
Logincomponent surfaces login and challenge errors instead of failing silently.
Dependencies
componentsnow requiresantd@^6.4.5(was^6.4.3). As part of the same sweep,startermigrated off the deprecated antdAlertmessage/onCloseprops totitle/closable— if your app still uses the deprecated props on its own alerts, plan the same migration.
v2.6.0 (2026-06-25)
No breaking changes.
Features
- form-editor —
FormEditorgainedpublishTextandpublishLoadingprops for customizing the toolbar's publish action. - form-editor — table-storage column types are now inferred from field widgets instead of defaulting untyped.
v2.5.0 (2026-06-25)
Behavior change: AppItem.icon is a ReactNode
The starter app switcher's AppItem.icon changed from a kebab-case lucide icon name (icon?: string, resolved through DynamicIcon) to a render-ready node (icon?: ReactNode, rendered as-is). A plain string still type-checks — but it now renders as literal text. Released as a minor, yet hosts passing icon names must update:
// Before (≤ v2.4.4)
{ id: "crm", name: "CRM", icon: "briefcase" }
// After (≥ v2.5.0)
{ id: "crm", name: "CRM", icon: <DynamicIcon name="briefcase" /> }
Features
- approval-flow-editor —
ApprovalFlowEditorgained anonPublishprop ((definition: FlowDefinition) => void); the toolbar's publish button renders only when it is provided.
v2.4.1 – v2.4.4 (2026-06-17 – 2026-06-18)
Breaking (v2.4.1): slimmed ZEN expression runtime
@vef-framework-react/expression was cut down to the bare evaluator, and form-editor linkage was routed through the slimmed runtime. Removed from the public surface: the condition compiler (compileCondition, compileGroup, compileBranch, selectBranch, toZenLiteral), the intellisense API (analyzeTypes, getCompletionItems, getDiagnostics, …), the locale helpers, and the React bindings. All of the remainder was deleted in v2.7.0, so treat any dependency on this package as an upgrade blocker there.
Features
- form-editor — docked and drawer editor layout modes, per-column width for table subforms, a wider collapsed palette rail, and table subforms displayed as a column table in the designer (all v2.4.1).
- components —
ScrollAreagained ascrollbarsaxis option (v2.4.1).
Fixes worth knowing
- starter — a series of top-menu fixes: overflowing items fold into the rest indicator without spillover (v2.4.2, v2.4.3), menu access checks match against the route template (v2.4.2), and menus are identified by path + params + search so parameterized routes highlight distinctly (v2.4.3).
UserMenu.metaparams/search were typed accordingly (v2.4.4). - core — a token-expired request now resolves with the retried response after a silent refresh, instead of surfacing the original 401 (v2.4.2).
- build — package type declarations are kept flat under
dist/types, fixing broken type resolution in consumers (v2.4.1).
Earlier releases (before v2.4.0)
v2.4.0 (2026-06-15) is the initial public release; the repository's git history starts there, so earlier changes are summarized from the documentation rather than from commits.
The one earlier migration you will still encounter in these docs: v2.1.6 moved the VEF page and CRUD components from @vef-framework-react/starter to @vef-framework-react/components — Page, Crud / CrudPage, ProTable, ProSearch, FlexCard, FormModal, and FormDrawer — and introduced LogoIcon in components. If you are coming from a pre-2.1.6 codebase, update those imports from starter to components; each affected page in Components carries a note to the same effect.