Skip to main content

Detail Page: Viewing and Editing Data

What You'll Learn in This Section
  • Using getOne to query a single record
  • Using update to update data
  • Using delete to delete data
  • Implementing view/edit mode switching

Estimated Time: 20 minutes


What Are We Building?

In the previous section, we implemented the customer list. Now let's build the detail page:

  • Display complete information for a single customer
  • Support editing (click to switch to edit mode)
  • Support deletion (with confirmation)
  • Return to the list after successful operations

Customer Detail Page


Step 1: Configure Routes

The detail page needs to fetch data using the ID from the URL, so let's configure the route first.

// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import CustomerList from "./pages/customer-list";
import CustomerDetail from "./pages/customer-detail";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<CustomerList />} />
<Route path="/customer/:id" element={<CustomerDetail />} />
</Routes>
</BrowserRouter>
);
}

export default App;
Route Parameter Explanation
  • /customer/:id - :id is a dynamic parameter that can be retrieved in the component using useParams()
  • When accessing /customer/123, id will be 123

Step 2: Fetch Data Details

Create the detail page component and use getOne to fetch a single record:

// src/pages/customer-detail.tsx

/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";

// Customer data type
interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
update_time: string;
}

export default function CustomerDetail() {
// Get customer ID from URL
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);

// Fetch customer details
useEffect(() => {
async function fetchCustomer() {
if (!id) return;

try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist or has been deleted");
navigate("/");
} finally {
setLoading(false);
}
}

fetchCustomer();
}, [id]);

if (loading) {
return <div style={{ padding: 24 }}>Loading...</div>;
}

if (!customer) {
return <div style={{ padding: 24 }}>Data does not exist</div>;
}

return (
<div style={{ padding: 24 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>Customer Details</h1>

<div style={{ marginTop: 16 }}>
<p><strong>Name:</strong>{customer.name}</p>
<p><strong>Phone:</strong>{customer.phone}</p>
<p><strong>Company:</strong>{customer.company || "-"}</p>
<p><strong>Status:</strong>{customer.status}</p>
<p><strong>Created At:</strong>{customer.create_time}</p>
</div>
</div>
);
}
Code Explanation
  • useParams() - Retrieves the :id parameter from the URL
  • useNavigate() - Programmatic navigation for page transitions
  • .getOne<Customer>(id) - Fetches a single record by ID, supports generics
  • getOne returns the data object directly, not a { data: ... } wrapper structure

Step 3: Add Edit Functionality

Now let users edit customer information. We'll implement this by switching between "view mode" and "edit mode":

// src/pages/customer-detail.tsx

/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";

interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
}

const STATUS_OPTIONS = [
{ value: "potential", label: "Potential Customer" },
{ value: "active", label: "Active Customer" },
{ value: "lost", label: "Lost Customer" },
];

export default function CustomerDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);

// New: Edit-related state
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<Customer>>({});
const [saving, setSaving] = useState(false);

// Fetch customer details
useEffect(() => {
async function fetchCustomer() {
if (!id) return;

try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
setEditForm(data); // Initialize edit form
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist");
navigate("/");
} finally {
setLoading(false);
}
}

fetchCustomer();
}, [id]);

// New: Save edits
const handleSave = async () => {
if (!id) return;

setSaving(true);
try {
// Call update API
const updated = await lovrabetClient.models.customers.update(id, editForm);
setCustomer(updated); // Update display data
setIsEditing(false); // Exit edit mode
alert("Saved successfully!");
} catch (error) {
console.error("Failed to save:", error);
alert("Failed to save, please try again");
} finally {
setSaving(false);
}
};

// New: Cancel editing
const handleCancel = () => {
setEditForm(customer || {}); // Restore original data
setIsEditing(false);
};

if (loading) return <div style={{ padding: 24 }}>Loading...</div>;
if (!customer) return <div style={{ padding: 24 }}>Data does not exist</div>;

return (
<div style={{ padding: 24 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>Customer Details</h1>

{isEditing ? (
// Edit mode
<div style={{ marginTop: 16 }}>
<div style={formGroupStyle}>
<label style={labelStyle}>Name</label>
<input
style={inputStyle}
value={editForm.name || ""}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
</div>

<div style={formGroupStyle}>
<label style={labelStyle}>Phone</label>
<input
style={inputStyle}
value={editForm.phone || ""}
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
/>
</div>

<div style={formGroupStyle}>
<label style={labelStyle}>Company</label>
<input
style={inputStyle}
value={editForm.company || ""}
onChange={(e) => setEditForm({ ...editForm, company: e.target.value })}
/>
</div>

<div style={formGroupStyle}>
<label style={labelStyle}>Status</label>
<select
style={inputStyle}
value={editForm.status || ""}
onChange={(e) => setEditForm({ ...editForm, status: e.target.value as any })}
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>

<div style={{ marginTop: 24 }}>
<button onClick={handleSave} disabled={saving} style={{ marginRight: 8 }}>
{saving ? "Saving..." : "Save"}
</button>
<button onClick={handleCancel} disabled={saving}>
Cancel
</button>
</div>
</div>
) : (
// View mode
<div style={{ marginTop: 16 }}>
<p><strong>Name:</strong>{customer.name}</p>
<p><strong>Phone:</strong>{customer.phone}</p>
<p><strong>Company:</strong>{customer.company || "-"}</p>
<p><strong>Status:</strong>{
STATUS_OPTIONS.find(o => o.value === customer.status)?.label
}</p>
<p><strong>Created At:</strong>{
new Date(customer.create_time).toLocaleString()
}</p>

<div style={{ marginTop: 24 }}>
<button onClick={() => setIsEditing(true)} style={{ marginRight: 8 }}>
Edit
</button>
</div>
</div>
)}
</div>
);
}

// Simple styles
const formGroupStyle: React.CSSProperties = { marginBottom: 16 };
const labelStyle: React.CSSProperties = { display: "block", marginBottom: 4, fontWeight: "bold" };
const inputStyle: React.CSSProperties = { padding: 8, width: 300 };
Code Explanation
  • update(id, data) - Updates the record with the specified ID
  • Only pass the fields that need to be modified, no need to pass all fields
  • Returns the complete updated data (including auto-updated update_time)
  • Use a separate editForm state for editing; cancel restores original data to avoid polluting the display

Step 4: Add Delete Functionality

Delete operations should be handled carefully, add a confirmation dialog:

// Add to CustomerDetail component

// Delete customer
const handleDelete = async () => {
if (!id) return;

// Confirmation dialog
const confirmed = window.confirm("Are you sure you want to delete this customer? This action cannot be undone!");
if (!confirmed) return;

try {
await lovrabetClient.models.customers.delete(id);
alert("Deleted successfully!");
navigate("/"); // Return to list
} catch (error) {
console.error("Failed to delete:", error);
alert("Failed to delete, please try again");
}
};

// Add delete button in JSX
<button onClick={handleDelete} style={{ marginLeft: 8, color: "red" }}>
Delete
</button>
Note

Delete functionality is only available in WebAPI mode (Cookie authentication).

If you're using OpenAPI mode (AccessKey authentication), delete operations will result in an error. In this case, consider using "soft delete" — add a deleted field to mark the deletion status.


Step 5: Add Navigation from List Page

Finally, let each row in the list page be clickable to navigate to the detail page:

// Modify in src/pages/customer-list.tsx

import { useNavigate } from "react-router-dom";

export default function CustomerList() {
const navigate = useNavigate();

// ... other code ...

return (
<table>
<tbody>
{customers.map((customer) => (
<tr
key={customer.id}
onClick={() => navigate(`/customer/${customer.id}`)}
style={{ cursor: "pointer" }} // Change cursor to pointer
>
<td>{customer.name}</td>
<td>{customer.phone}</td>
{/* ... */}
</tr>
))}
</tbody>
</table>
);
}

Complete Code

Click to view complete code
// src/pages/customer-detail.tsx

/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";

interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
update_time: string;
}

const STATUS_OPTIONS = [
{ value: "potential", label: "Potential Customer" },
{ value: "active", label: "Active Customer" },
{ value: "lost", label: "Lost Customer" },
];

export default function CustomerDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<Customer>>({});
const [saving, setSaving] = useState(false);

// Fetch data
useEffect(() => {
async function fetchCustomer() {
if (!id) return;

setLoading(true);
try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
setEditForm(data);
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist");
navigate("/");
} finally {
setLoading(false);
}
}

fetchCustomer();
}, [id]);

// Save
const handleSave = async () => {
if (!id) return;
setSaving(true);
try {
const updated = await lovrabetClient.models.customers.update(id, editForm);
setCustomer(updated);
setIsEditing(false);
alert("Saved successfully!");
} catch (error) {
console.error("Failed to save:", error);
alert("Failed to save, please try again");
} finally {
setSaving(false);
}
};

// Cancel editing
const handleCancel = () => {
setEditForm(customer || {});
setIsEditing(false);
};

// Delete
const handleDelete = async () => {
if (!id) return;
if (!window.confirm("Are you sure you want to delete? This action cannot be undone!")) return;

try {
await lovrabetClient.models.customers.delete(id);
alert("Deleted successfully!");
navigate("/");
} catch (error) {
console.error("Failed to delete:", error);
alert("Failed to delete, please try again");
}
};

if (loading) return <div style={{ padding: 24 }}>Loading...</div>;
if (!customer) return <div style={{ padding: 24 }}>Data does not exist</div>;

const getStatusLabel = (status: string) =>
STATUS_OPTIONS.find(o => o.value === status)?.label || status;

return (
<div style={{ padding: 24, maxWidth: 600 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>{isEditing ? "Edit Customer" : "Customer Details"}</h1>

{isEditing ? (
<div style={{ marginTop: 16 }}>
<FormField label="Name">
<input
value={editForm.name || ""}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
</FormField>
<FormField label="Phone">
<input
value={editForm.phone || ""}
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
/>
</FormField>
<FormField label="Company">
<input
value={editForm.company || ""}
onChange={(e) => setEditForm({ ...editForm, company: e.target.value })}
/>
</FormField>
<FormField label="Status">
<select
value={editForm.status || ""}
onChange={(e) => setEditForm({ ...editForm, status: e.target.value as any })}
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</FormField>
<div style={{ marginTop: 24 }}>
<button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
<button onClick={handleCancel} style={{ marginLeft: 8 }}>Cancel</button>
</div>
</div>
) : (
<div style={{ marginTop: 16 }}>
<InfoRow label="Name" value={customer.name} />
<InfoRow label="Phone" value={customer.phone} />
<InfoRow label="Company" value={customer.company || "-"} />
<InfoRow label="Status" value={getStatusLabel(customer.status)} />
<InfoRow label="Created At" value={new Date(customer.create_time).toLocaleString()} />
<div style={{ marginTop: 24 }}>
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={handleDelete} style={{ marginLeft: 8, color: "red" }}>
Delete
</button>
</div>
</div>
)}
</div>
);
}

// Helper components
function FormField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ marginBottom: 16 }}>
<label style={{ display: "block", marginBottom: 4, fontWeight: "bold" }}>{label}</label>
<div>{children}</div>
</div>
);
}

function InfoRow({ label, value }: { label: string; value: string }) {
return (
<p style={{ margin: "8px 0" }}>
<strong>{label}:</strong>{value}
</p>
);
}

Section Summary

You learned:

APIPurposeReturn Value
getOne(id)Fetch a single recordThe data object itself
update(id, data)Update dataComplete updated data
delete(id)Delete dataNone (WebAPI mode only)

FAQ

Q: What's the difference between data returned by getOne and filter?

  • getOne(id) - Returns the data object directly { id, name, ... }
  • filter() - Returns a pagination structure { tableData, total, ... }

Q: Do I need to pass all fields when updating?

No. Only pass the fields you want to modify; other fields will remain unchanged. This also helps avoid data overwriting issues during concurrent modifications.

Q: What should I do if delete functionality fails?

Delete functionality is only available in WebAPI mode (Cookie authentication). If you're using OpenAPI mode, consider using "soft delete":

// Soft delete: Mark as deleted instead of physical deletion
await lovrabetClient.models.customers.update(id, {
status: "deleted",
deleted_at: new Date().toISOString()
});

Next Steps