fix: restrict edit mode for reference items to personal fields only
Reference items (linked to global catalog) now show name, brand, weight, and MSRP as read-only in edit mode with "from the catalog" hint. Only personal fields (notes, category, quantity, image, product URL) are editable. Standalone items retain full edit access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ export const Route = createFileRoute("/items/$itemId")({
|
||||
|
||||
interface EditFormState {
|
||||
name: string;
|
||||
brand: string;
|
||||
weightGrams: string;
|
||||
priceDollars: string;
|
||||
quantity: number;
|
||||
@@ -45,6 +46,7 @@ function ItemDetail() {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [form, setForm] = useState<EditFormState>({
|
||||
name: "",
|
||||
brand: "",
|
||||
weightGrams: "",
|
||||
priceDollars: "",
|
||||
quantity: 1,
|
||||
@@ -57,7 +59,8 @@ function ItemDetail() {
|
||||
function enterEditMode() {
|
||||
if (!item) return;
|
||||
setForm({
|
||||
name: item.name,
|
||||
name: item.brand ? item.name.replace(`${item.brand} `, "") : item.name,
|
||||
brand: item.brand || "",
|
||||
weightGrams: item.weightGrams != null ? String(item.weightGrams) : "",
|
||||
priceDollars:
|
||||
item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "",
|
||||
@@ -76,29 +79,41 @@ function ItemDetail() {
|
||||
|
||||
function handleSave() {
|
||||
if (!item) return;
|
||||
const hasGlobalItem = !!(item as Record<string, unknown>).globalItemId;
|
||||
|
||||
// Personal fields (always editable)
|
||||
const payload: Record<string, unknown> = {
|
||||
id: item.id,
|
||||
quantity: form.quantity,
|
||||
categoryId: form.categoryId,
|
||||
notes: form.notes.trim() || undefined,
|
||||
productUrl: form.productUrl.trim() || undefined,
|
||||
imageFilename: form.imageFilename ?? undefined,
|
||||
};
|
||||
|
||||
// Global fields (only editable for standalone items)
|
||||
if (!hasGlobalItem) {
|
||||
const weightGrams = form.weightGrams.trim()
|
||||
? Math.round(Number(form.weightGrams))
|
||||
: null;
|
||||
const priceCents = form.priceDollars.trim()
|
||||
? Math.round(Number(form.priceDollars) * 100)
|
||||
: null;
|
||||
const brandTrimmed = form.brand.trim();
|
||||
const nameTrimmed = form.name.trim();
|
||||
const fullName = brandTrimmed
|
||||
? `${brandTrimmed} ${nameTrimmed}`
|
||||
: nameTrimmed;
|
||||
|
||||
updateItem.mutate(
|
||||
{
|
||||
id: item.id,
|
||||
name: form.name.trim(),
|
||||
weightGrams,
|
||||
priceCents,
|
||||
quantity: form.quantity,
|
||||
categoryId: form.categoryId,
|
||||
notes: form.notes.trim() || null,
|
||||
productUrl: form.productUrl.trim() || null,
|
||||
imageFilename: form.imageFilename,
|
||||
},
|
||||
{
|
||||
payload.name = fullName;
|
||||
payload.brand = brandTrimmed || undefined;
|
||||
payload.weightGrams = weightGrams ?? undefined;
|
||||
payload.priceCents = priceCents ?? undefined;
|
||||
}
|
||||
|
||||
updateItem.mutate(payload as Parameters<typeof updateItem.mutate>[0], {
|
||||
onSuccess: () => setIsEditing(false),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDuplicate() {
|
||||
@@ -156,6 +171,7 @@ function ItemDetail() {
|
||||
}
|
||||
|
||||
const imageUrl = item.imageUrl || null;
|
||||
const isReference = !!(item as Record<string, unknown>).globalItemId;
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
@@ -248,21 +264,88 @@ function ItemDetail() {
|
||||
{/* Header / Name */}
|
||||
<div className="mb-4">
|
||||
{isEditing ? (
|
||||
isReference ? (
|
||||
/* Reference items: name/brand are read-only (from global catalog) */
|
||||
<>
|
||||
{item.brand && (
|
||||
<p className="text-sm font-medium text-gray-400 uppercase tracking-wide mb-1">
|
||||
{item.brand}
|
||||
</p>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{item.brand
|
||||
? item.name.replace(`${item.brand} `, "")
|
||||
: item.name}
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Name and brand are from the catalog
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={form.brand}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, brand: e.target.value }))
|
||||
}
|
||||
className="w-full text-sm font-medium text-gray-500 border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
||||
placeholder="Brand / Manufacturer (optional)"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, name: e.target.value }))
|
||||
}
|
||||
className="w-full text-2xl font-bold text-gray-900 border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
|
||||
placeholder="Item name"
|
||||
placeholder="Item name / Model"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<h1 className="text-2xl font-bold text-gray-900">{item.name}</h1>
|
||||
<>
|
||||
{item.brand && (
|
||||
<p className="text-sm font-medium text-gray-400 uppercase tracking-wide mb-1">
|
||||
{item.brand}
|
||||
</p>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{item.brand ? item.name.replace(`${item.brand} `, "") : item.name}
|
||||
</h1>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges / Specs */}
|
||||
{isEditing ? (
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{isReference ? (
|
||||
/* Reference items: weight/price are read-only (from catalog) */
|
||||
<>
|
||||
{item.weightGrams != null && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Weight (g)
|
||||
</label>
|
||||
<p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
|
||||
{item.weightGrams}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.priceCents != null && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
MSRP
|
||||
</label>
|
||||
<p className="py-2 px-3 bg-gray-50 border border-gray-100 rounded-lg text-sm text-gray-500">
|
||||
{price(item.priceCents)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Weight (g)
|
||||
@@ -298,6 +381,8 @@ function ItemDetail() {
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Quantity
|
||||
|
||||
Reference in New Issue
Block a user