Shell Context & State Management API
This reference documents the React hooks and context injected by the Framework M Desk shell. Use these hooks when building Micro-Frontends (MFEs), plugin components, or custom page overrides so that generated code matches the actual runtime contract.
1. useShell()
Access the global shell context inside any component rendered below <ShellProvider>.
import { useShell } from "@framework-m/desk";
function MyWidget() {
const shell = useShell();
// shell.isMobile, shell.registerSection, ...
}
ShellContextValue
| Property | Type | Description |
|---|---|---|
isMobile | boolean | Whether the shell is rendering in mobile viewport mode. |
isGuest | boolean | Whether the current session is an anonymous guest session. |
rightSidebarOpen | boolean | Current open/closed state of the right sidebar. |
setRightSidebarOpen | (value: boolean) => void | Setter for the right sidebar state. |
focusMode | boolean | Whether focus mode (distraction-free UI) is active. |
setFocusMode | (value: boolean) => void | Explicitly enable/disable focus mode. |
toggleFocusMode | () => void | Toggle focus mode. |
registerSection | (section: ShellSection) => () => void | Register a navigation section. Returns an unregister function. |
getSections | (slot: ShellSection["slot"]) => ShellSection[] | Retrieve sections for a given navbar/sidebar slot. |
registerOmniSearchProvider | (provider: OmniSearchProvider) => () => void | Register a global omni-search source. |
searchOmni | (query: string) => Promise<OmniSearchResult[]> | Execute omni-search across all registered providers. |
setRealtimeAdapter | (adapter: RealtimeAdapter | null) => Promise<void> | Swap the realtime/WebSocket adapter at runtime. |
subscribeRealtime | (room: string, listener: RealtimeSubscriber) => () => void | Subscribe to a realtime room. Returns an unsubscribe function. |
publishRealtime | (room: string, type: string, payload: Record<string, unknown>) => Promise<void> | Publish a message to a realtime room. |
ShellSection
interface ShellSection {
id: string;
slot: "navbar-left" | "navbar-right" | "sidebar";
order?: number;
label?: string;
icon?: string;
route?: string;
module?: string;
render?: () => ReactNode;
}
OmniSearchResult
interface OmniSearchResult {
id: string;
label: string;
route: string;
}
1.1 Auth & Login Guard: AuthenticatedLoginGuard
Use AuthenticatedLoginGuard in custom public or login routes to prevent already-authenticated users from seeing the login screen, while preserving target query parameters (?to= or ?redirect=).
import { AuthenticatedLoginGuard } from "@framework-m/desk";
// Custom public login route in plugin.config.ts or shell app
<Route
path="/portal/login"
element={
<AuthenticatedLoginGuard
loginPage={<CustomPortalLoginPage />}
defaultTarget="/portal/dashboard"
/>
}
/>
AuthenticatedLoginGuardProps
| Property | Type | Default | Description |
|---|---|---|---|
loginPage | ReactNode | <LoginPage /> | Login page component rendered when the user is NOT authenticated. |
defaultTarget | string | "/app/dashboard" | Fallback route when no ?to= or ?redirect= query param is present. |
2. Form State: useFormController
Standard hook for reading and mutating a DocType record in a form view.
import { useFormController } from "@framework-m/desk";
function TaskEditor({ docname }: { docname: string }) {
const { frm, schema, metaLoading, metaError } = useFormController({
doctype: "Task",
id: docname,
});
if (metaLoading) return <div>Loading...</div>;
if (metaError) return <div>Error: {metaError.message}</div>;
return (
<>
<input
value={(frm.doc.title as string) ?? ""}
onChange={(e) => frm.setDoc({ title: e.target.value })}
/>
<button onClick={() => frm.save()} disabled={frm.isSaving}>
Save
</button>
</>
);
}
UseFormControllerProps
| Property | Type | Description |
|---|---|---|
doctype | string | DocType to load. Defaults to the doctype route param. |
id | string | Record identifier. "me" resolves to the current user. Defaults to the id route param. |
Frm object returned in { frm }
| Property | Type | Description |
|---|---|---|
doc | Record<string, unknown> | Current document values. |
setDoc | (update: Partial<T> | ((prev: T) => T)) => void | Patch document state. |
save | (options?, overrideValues?) => Promise<void> | Persist the document. |
cancel | () => void | Cancel editing and navigate back to list. |
isEditing | boolean | Whether an existing record is being edited. |
isSaving | boolean | Whether a save mutation is in flight. |
isLocked | boolean | Whether the document is submitted/cancelled (docstatus 1 or 2). |
errors | string[] | Top-level error messages from the last failed save. |
notices | string[] | Informational notices. |
validationError | ValidationError | null | Structured validation errors from the backend. |
workflow | unknown | Workflow state if applicable. |
refetch | () => Promise<void> | Refetch the current record. |
3. DocType Metadata: useDocTypeMeta
Fetch schema, layout, permissions, and workflow metadata for a DocType.
import { useDocTypeMeta } from "@framework-m/desk";
function TaskMeta() {
const { schema, layout, permissions, isLoading, error } = useDocTypeMeta("Task");
// ...
}
UseDocTypeMetaResult
| Property | Type | Description |
|---|---|---|
schema | DocTypeSchema | undefined | JSON Schema for the DocType. |
layout | LayoutConfig | undefined | Layout configuration for form rendering. |
permissions | DocTypePermissions | undefined | Current user's permissions. |
workflow | WorkflowConfig | undefined | Workflow transitions and states. |
meta | DocTypeMeta | undefined | Full raw metadata object. |
isLoading | boolean | Loading state. |
error | Error | null | Error if fetch failed. |
refetch | () => void | Trigger a metadata refetch. |
4. RPC Calls: useCall
General-purpose hook for calling RPC endpoints or arbitrary API paths.
import { useCall } from "@framework-m/desk";
function ApproveButton({ docnames }: { docnames: string[] }) {
const { execute, isLoading, data, error } = useCall<{ message: string }>();
const approve = async () => {
const result = await execute({
doctype: "Task",
method: "bulk_approve",
payload: { docnames },
});
if (result) {
// success
}
};
return <button onClick={approve} disabled={isLoading}>Approve</button>;
}
UseCallResult<T>
| Property | Type | Description |
|---|---|---|
execute | (args: CallArgs) => Promise<T | null> | Execute the request. |
isLoading | boolean | Request in-flight flag. |
data | T | null | Last successful response data. |
error | string | null | Last error message. |
CallArgs
Either an RPC-style call:
interface RPCCallArgs {
doctype: string;
method: string;
payload?: unknown;
httpMethod?: string;
}
Or a path-based call:
interface PathCallArgs {
path: string;
method?: string;
payload?: unknown;
}
5. View Customization: Simple (Low-Code) vs Expert Modes
Framework M supports a 2-tier architecture: Simple / Low-Code Mode for 80% standard CRUD screens, and Expert Mode for 20% custom enterprise React screens. Top breadcrumbs and shell layout features are maintained at the shell level for both modes.
Tier 1: Simple / Low-Code Mode (Fast Prototyping)
For 80% of standard DocTypes (ItemGroup, TaxRate, Company, LocalUser), zero React code is required. Views render automatically from DocType metadata schemas.
When a low-code developer needs to tweak a specific view, they declare a component override in plugin.config.ts while using Frappe-style frm controller helpers (frm.setDocValue(), frm.save(), frm.doc):
// plugin.config.ts (Low-Code View Override)
const plugin: FrameworkMPlugin = {
name: "wms",
version: "0.1.0",
doctypes: [
{
doctype: "StockEntry",
components: {
FormView: WmsStockEntryFormView, // Custom low-code form view override
},
},
],
};
// Form scripting with Frappe-style `frm` helper:
export function WmsStockEntryFormView({ doctype, id }: { doctype: string; id?: string }) {
const { frm, schema } = useFormController({ doctype, id });
const handleQuickSubmit = () => {
frm.setDocValue("docstatus", 1);
frm.save();
};
return (
</div>
);
}
Auto-Discovery Rules (Directory-based vs plugin.config.ts)
To avoid developer confusion, choose one of the following registration patterns:
-
Automatic Directory Discovery (
src/overrides/[DocType]/):@framework-m/vite-pluginautomatically detects override components placed in standard directory locations—no manual registration inplugin.config.tsrequired:src/overrides/StockEntry/FormView.tsx(Form view override)src/overrides/StockEntry/KanbanView.tsx(Kanban view override)src/overrides/Core/LoginPage.tsx(Global login page override)
-
Explicit Declaration (
plugin.config.ts): Usedoctypes[].componentsinplugin.config.tswhen your component lives outsidesrc/overrides/(e.g.src/pages/WmsStockEntryFormView.tsx).
Do not double-register the same component in both src/overrides/[DocType]/ and plugin.config.ts. Choose either convention per plugin.
Tier 2: Expert Mode (React Routes & Atomic Primitives)
For 20% complex enterprise screens (e.g. SupplierProfilePage with maps, charts, or Tamagui stacks), senior developers declare standard React routes in plugin.config.ts and compose pages using atomic primitives (FormActions, FormFields, ListSearchInput, ListPagination, KanbanCardItem, TreeNodeItem):
// plugin.config.ts (Expert React Route Declaration)
const plugin: FrameworkMPlugin = {
name: "supplier_app",
version: "0.1.0",
routes: [
{
path: "/supplier_app/SupplierProfile/:id",
element: () => import("./pages/SupplierProfilePage"),
},
],
};
// pages/SupplierProfilePage.tsx (Expert Composition)
import { useFormController, FormActions, FormFields } from "@framework-m/desk";
import { YStack, XStack, Heading, Card } from "@framework-m/ui";
export function SupplierProfilePage() {
const controller = useFormController({ doctype: "SupplierProfile" });
return (
<YStack gap="$md">
<Card padding="$md">
<Heading size="$md">{controller.frm.doc?.supplier_name || "Supplier Profile"}</Heading>
<FormActions controller={controller} />
</Card>
<XStack gap="$lg">
<FormFields controller={controller} />
{/* Custom React / Map widget */}
<MyMapWidget location={controller.frm.doc?.location} />
</XStack>
</YStack>
);
}
6. List View Actions: useListActions
State helpers for list views (column preferences, view mode, row click navigation).
import { useListActions } from "@framework-m/desk";
function TaskList() {
const {
activeView,
setActiveView,
handleRowClick,
columnPreferences,
toggleColumnVisibility,
isMobile,
} = useListActions({
doctype: "Task",
canKanban: true,
canTree: false,
canGantt: false,
canCalendar: false,
});
// ...
}
UseListActionsProps
| Property | Type | Description |
|---|---|---|
doctype | string | DocType being listed. |
propView | ListViewMode | Optional controlled view mode. |
canKanban | boolean | Whether kanban view is available. |
canTree | boolean | Whether tree view is available. |
canGantt | boolean | Whether gantt view is available. |
canCalendar | boolean | Whether calendar view is available. |
resolveDetailRoute | (doctype, id, row) => string | undefined | Optional custom detail route resolver. |
7. Routing & Navigation
The shell does not expose imperative navigation helpers on ShellContextValue. Use React Router directly:
import { useNavigate, Link } from "react-router";
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const navigate = useNavigate();
return <button onClick={() => navigate(to)}>{children}</button>;
}
Use the exported <Breadcrumb /> component from @framework-m/desk to render breadcrumbs driven by @refinedev/core's useBreadcrumb hook.
8. Feedback Components
For notifications, toasts, and confirmations, use the feedback primitives exported by @framework-m/desk:
AlertBanner— inline status/error/success banner.ConfirmDialog— modal confirmation dialog.SubmittedBanner— banner shown after successful submission.
There is no useToast() hook in @framework-m/desk.