Skip to main content

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

ParameterTypeDescription
funcfunctionAsync function that handles validation and saving. Can throw errors to prevent modal close
stateTriggersarrayOptional 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 onSave callback 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

VariantUse Case
primaryPrimary information or identifiers
successActive, running, or success states
warningWarnings or attention needed
dangerErrors or critical states
alternateAlternative information
subtleLow-emphasis indicators
highlightHigh-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;