Sidesheets
Sidesheets are modals that slide in from the right side of the screen, providing a focused interface for viewing details, editing data, or creating new records. They stack on top of each other and can include pagination for navigating between related items.
Key Capabilities
- View information - Display detailed content in a focused panel
- Edit data - Toggle between read-only and editable states
- Create records - Open directly in edit mode for new items
- Stack navigation - Open sidesheets from within other sidesheets
- Pagination - Navigate between related items without closing the modal
- Dynamic headers - Set custom titles and badges that update based on state
Opening a Sidesheet
Use the launchModal command from useMMAppCommands:
import { useMMAppCommands } from '@machinemetrics/mm-react-tools';
function MachineList() {
const { launchModal } = useMMAppCommands();
const handleViewMachine = async (machineId) => {
await launchModal(`${window.location.origin}/machines/${machineId}`, {
title: 'Machine Details',
});
// Continues when sidesheet is closed
console.log('Sidesheet closed');
};
return (
<button onClick={() => handleViewMachine(123)}>
View Machine
</button>
);
}
Important: URLs must be fully qualified including the origin (e.g., ${window.location.origin}/path).
Sidesheet Modes
Sidesheets can operate in three different modes depending on your needs.
View-Only
Opens for viewing information without editing capabilities. No Edit button is shown.
// Don't provide an onSave handler for view-only
launchModal(`${window.location.origin}/reports/123`, {
title: 'Production Report',
});
View and Edit
Opens in view mode with an Edit button. Users can toggle to edit mode, make changes, and save or cancel.
launchModal(`${window.location.origin}/machines/${machineId}`, {
title: 'Machine XYZ-123',
editTitle: 'Edit Machine',
onSave: (updatedData) => {
console.log('Saved:', updatedData);
},
});
The modal starts in view mode. When the user clicks Edit, isEditing becomes true. Your component responds by showing editable fields:
import { useMMAppParams, useMMAppModalSave } from '@machinemetrics/mm-react-tools';
function MachineDetailsModal() {
const { isEditing } = useMMAppParams();
const [formData, setFormData] = useState({});
useMMAppModalSave(async () => {
if (!formData.name) {
throw new Error('Name is required');
}
await saveMachine(formData);
return formData;
}, [formData]);
return (
<div>
{isEditing ? (
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
) : (
<h2>{formData.name}</h2>
)}
</div>
);
}
Edit-Only
Opens directly in edit mode for creating new records or one-time edits. Saving or canceling closes the modal.
launchModal(`${window.location.origin}/machines/create`, {
title: 'Create Machine',
editOnly: true,
onSave: (newMachine) => {
console.log('Created:', newMachine);
},
});
The modal opens with isEditing: true from the start, and closes immediately after save or cancel.
Handling Saves
Use useMMAppModalSave to handle the save button click. This hook registers a handler that's called when the user clicks Save.
import { useMMAppModalSave } from '@machinemetrics/mm-react-tools';
function EditMachineModal() {
const [formData, setFormData] = useState({});
useMMAppModalSave(
async () => {
// Validate
if (!formData.name) {
throw new Error('Name is required');
}
// Save
const saved = await api.saveMachine(formData);
// Return to onSave callback
return saved;
},
[formData] // Re-register when dependencies change
);
return <form>{/* fields */}</form>;
}
Parameters
| Parameter | Type | Description |
|---|---|---|
func | function | Async function that handles validation and saving. Can throw errors to prevent modal close |
stateTriggers | array | Optional array of dependencies. Handler will be re-registered when these values change |
Error Handling
When the handler function throws an error:
- A toast notification is displayed to the user with the error message
- The modal remains open, allowing the user to correct issues
- The
onSavecallback is not called
useMMAppModalSave(async () => {
if (!isValid) {
// Shows a toast and keeps the modal open
throw new Error('Please correct the validation errors');
}
return savedData;
}, [formData]);
Validation Only
The handler can perform validation without saving:
useMMAppModalSave(async () => {
const validationErrors = validateForm(formData);
if (validationErrors.length > 0) {
setErrors(validationErrors);
throw new Error('Form validation failed');
}
// Return data for parent component to save
return { ...formData, validatedAt: new Date() };
}, [formData]);
With UI Updates
function EditMachineSettings() {
const [settings, setSettings] = useState({});
const [errors, setErrors] = useState([]);
useMMAppModalSave(
async () => {
setErrors([]);
const validationErrors = await validateSettings(settings);
if (validationErrors.length > 0) {
setErrors(validationErrors);
throw new Error('Please fix validation errors');
}
const result = await api.updateMachineSettings(settings);
return result;
},
[settings]
);
return (
<div>
{errors.map((error) => (
<div key={error.field} className="error">{error.message}</div>
))}
{/* Form fields */}
</div>
);
}
Badges
Display status indicators and metadata in the sidesheet header using useMMAppInfo:
import { useMMAppInfo } from '@machinemetrics/mm-react-tools';
function MachineModal() {
const [machine, setMachine] = useState(null);
useMMAppInfo(
{
title: machine?.name,
badges: [
{ variant: 'success', label: 'Running' },
{ variant: 'primary', label: 'Line 3' },
],
},
[machine]
);
return <div>{/* content */}</div>;
}
Available Badge Variants
| Variant | Use Case |
|---|---|
primary | Primary information or identifiers |
success | Active, running, or success states |
warning | Warnings or attention needed |
danger | Errors or critical states |
alternate | Alternative information |
subtle | Low-emphasis indicators |
highlight | High-emphasis information |
Badges update dynamically as your component state changes.
Pagination
Allow users to navigate between related items without closing the sidesheet:
function PartsList() {
const { launchModal } = useMMAppCommands();
const [parts, setParts] = useState([]);
const handleViewPart = (partId) => {
const origin = window.location.origin;
const pagination = parts.map(part => ({
title: part.name,
url: `${origin}/parts/${part.id}`,
}));
launchModal(`${origin}/parts/${partId}`, {
title: 'Part Details',
pagination: pagination,
});
};
return (
<div>
{parts.map(part => (
<button key={part.id} onClick={() => handleViewPart(part.id)}>
{part.name}
</button>
))}
</div>
);
}
Previous and next arrows appear in the header. The platform navigates your embedded app's router to the new URL when the user clicks an arrow.
Note: Pagination arrows are hidden while in edit mode. Users must save or cancel before navigating to another item.
Your modal component should reload data when the route parameter changes:
function PartDetailsModal() {
const { partId } = useParams();
const [part, setPart] = useState(null);
useEffect(() => {
fetchPart(partId).then(setPart);
}, [partId]);
return <div>{/* details */}</div>;
}
Stacked Sidesheets
Open a sidesheet from within another sidesheet to create a navigation stack:
function MachineDetails() {
const { launchModal } = useMMAppCommands();
const handleViewPart = (partId) => {
launchModal(`${window.location.origin}/parts/${partId}`, {
title: 'Part Details',
});
};
return (
<div>
<h2>Machine XYZ-123</h2>
<button onClick={() => handleViewPart(456)}>
View Bearing Assembly
</button>
</div>
);
}
Users navigate back through the stack using the back arrow in each sidesheet header.
Complete Example
This example shows a parts list that opens a sidesheet for viewing and editing individual parts.
App.jsx
import { Routes, Route } from 'react-router-dom';
import PartsList from './components/PartsList';
import PartDetailsModal from './components/modals/PartDetailsModal';
function App() {
return (
<Routes>
<Route path="/" element={<PartsList />} />
<Route path="/parts/:partId" element={<PartDetailsModal />} />
</Routes>
);
}
export default App;
Note: BrowserRouter and MMEmbeddedAppProvider are provided at a higher level in the application entry point.
components/PartsList.jsx
import { useMMAppCommands } from '@machinemetrics/mm-react-tools';
import { useState, useEffect } from 'react';
function PartsList() {
const { launchModal } = useMMAppCommands();
const [parts, setParts] = useState([]);
useEffect(() => {
// Fetch parts on mount
fetchParts().then(setParts);
}, []);
const handleViewPart = (partId) => {
launchModal(`${window.location.origin}/parts/${partId}`, {
title: 'Part Details',
editTitle: 'Edit Part',
onSave: (updated) => {
setParts(parts.map(p => p.id === updated.id ? updated : p));
},
});
};
return (
<div className="p-6">
<h1>Parts Inventory</h1>
<div className="space-y-2">
{parts.map(part => (
<div
key={part.id}
className="cursor-pointer hover:bg-gray-100 p-4 rounded"
onClick={() => handleViewPart(part.id)}
>
<h3>{part.name}</h3>
<p className="text-sm text-gray-600">Quantity: {part.quantity}</p>
</div>
))}
</div>
</div>
);
}
export default PartsList;
components/modals/PartDetailsModal.jsx
import {
useMMAppParams,
useMMAppInfo,
useMMAppModalSave,
} from '@machinemetrics/mm-react-tools';
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
function PartDetailsModal() {
const { partId } = useParams();
const { isEditing } = useMMAppParams();
const [part, setPart] = useState(null);
const [formData, setFormData] = useState({});
useEffect(() => {
fetchPart(partId).then((data) => {
setPart(data);
setFormData(data);
});
}, [partId]);
useMMAppInfo(
{
title: part?.name,
badges: [
{ variant: 'success', label: part?.status },
],
},
[part]
);
useMMAppModalSave(
async () => {
if (!formData.name) {
throw new Error('Name is required');
}
const updated = await savePart(partId, formData);
setPart(updated);
return updated;
},
[formData, partId]
);
if (!part) return <div>Loading...</div>;
return (
<div className="p-6">
{isEditing ? (
<form className="space-y-4">
<div>
<label className="block text-sm font-medium">Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 block w-full rounded border-gray-300"
/>
</div>
<div>
<label className="block text-sm font-medium">Quantity</label>
<input
type="number"
value={formData.quantity}
onChange={(e) => setFormData({ ...formData, quantity: e.target.value })}
className="mt-1 block w-full rounded border-gray-300"
/>
</div>
</form>
) : (
<div className="space-y-4">
<h2 className="text-2xl font-bold">{part.name}</h2>
<div>
<p className="text-sm text-gray-600">Quantity</p>
<p className="text-lg">{part.quantity}</p>
</div>
<div>
<p className="text-sm text-gray-600">Status</p>
<p className="text-lg">{part.status}</p>
</div>
</div>
)}
</div>
);
}
export default PartDetailsModal;
Related Hooks
useMMAppCommands- IncludeslaunchModalandconfirmuseMMAppParams- ProvidesisEditingparameteruseMMAppInfo- Sets title and badges