feat(template): stable fieldId per field — custom_fields keyed by UUID, not label slug
This commit is contained in:
@@ -7,11 +7,12 @@ import { dailyStatusesApi } from '../api/client';
|
||||
|
||||
const BUILTIN_KEYS = new Set(['experiment_description', 'vitals', 'treatment', 'notes']);
|
||||
|
||||
/** Extract the value for a field from a status record */
|
||||
/** Extract the value for a field from a status record.
|
||||
* Custom fields are keyed by fieldId (stable UUID), not by key (display slug). */
|
||||
function getValue(status, field) {
|
||||
if (!status) return '';
|
||||
if (field.builtin) return status[field.key] ?? '';
|
||||
return status.custom_fields?.[field.key] ?? '';
|
||||
return status.custom_fields?.[field.fieldId] ?? '';
|
||||
}
|
||||
|
||||
export default function DailyStatusForm({ existing, animalId, template = [], onSuccess, onCancel }) {
|
||||
@@ -63,12 +64,17 @@ export default function DailyStatusForm({ existing, animalId, template = [], onS
|
||||
const custom = {};
|
||||
for (const f of activeFields) {
|
||||
const val = values[f.key] || null;
|
||||
if (f.builtin) builtin[f.key] = val;
|
||||
else custom[f.key] = val;
|
||||
if (f.builtin) {
|
||||
builtin[f.key] = val;
|
||||
} else {
|
||||
// Always key by fieldId so renaming or recreating a field never
|
||||
// collides with or orphans data from a different field.
|
||||
custom[f.fieldId] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with any pre-existing custom_fields not in current active template
|
||||
// (so we don't wipe data for fields that were hidden after the entry was made)
|
||||
// Preserve custom_fields for fields not in the current active template
|
||||
// (e.g. a field was hidden after this record was created).
|
||||
const existingCustom = existing?.custom_fields ?? {};
|
||||
const mergedCustom = { ...existingCustom, ...custom };
|
||||
|
||||
|
||||
@@ -53,7 +53,14 @@ function FieldRow({ field, onChange, onToggle, onRemove, allKeys }) {
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitLabel(); } }}
|
||||
/>
|
||||
{labelError && <p className="text-xs text-red-500 mt-0.5">{labelError}</p>}
|
||||
<p className="text-xs text-gray-400 mt-0.5 font-mono">key: {field.key}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 font-mono">
|
||||
key: {field.key}
|
||||
{field.fieldId && (
|
||||
<span className="ml-2 text-gray-300" title={`Stable field ID: ${field.fieldId}`}>
|
||||
· id: {field.fieldId.slice(0, 8)}…
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Type badge */}
|
||||
@@ -133,7 +140,8 @@ export default function TemplateEditor({ experimentId, onClose }) {
|
||||
if (!key) { setAddError('Name must contain at least one alphanumeric character'); return; }
|
||||
if (fields.some((f) => f.key === key)) { setAddError(`A field with key "${key}" already exists`); return; }
|
||||
setAddError('');
|
||||
setFields((prev) => [...prev, { key, label: trimmed, type: newType, builtin: false, active: true }]);
|
||||
const fieldId = crypto.randomUUID();
|
||||
setFields((prev) => [...prev, { fieldId, key, label: trimmed, type: newType, builtin: false, active: true }]);
|
||||
setNewLabel('');
|
||||
setNewType('text');
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import DailyStatusForm from '../components/DailyStatusForm';
|
||||
import Alert from '../components/ui/Alert';
|
||||
import AuditLogSection from '../components/AuditLogSection';
|
||||
|
||||
/** Render the value for a template field from a status record */
|
||||
/** Read the value for a template field from a status record.
|
||||
* Custom fields are keyed by fieldId, not by the display key. */
|
||||
function getFieldValue(status, field) {
|
||||
if (field.builtin) return status[field.key];
|
||||
return status.custom_fields?.[field.key];
|
||||
return status.custom_fields?.[field.fieldId];
|
||||
}
|
||||
|
||||
export default function AnimalDetail() {
|
||||
|
||||
Reference in New Issue
Block a user