Nova Damage Assessment Field Development
Trigger Phrases
- •"nova custom field"
- •"damage assessment editor"
- •"nova field component"
- •"inertia vue field"
- •"canvas field nova"
- •"custom nova field"
- •"field serialization"
- •"DetailField", "FormField", "IndexField"
Expertise
Expert in developing custom Laravel Nova 5.x fields with Vue 3 Composition API and Inertia 2.x, specifically for the PCR Card damage assessment bounding box editor system.
Project Context
PCR Card uses a sophisticated custom Nova field (DamageAssessmentEditor) that provides an interactive HTML5 Canvas interface for visually marking damage areas on trading card images.
Key Technologies:
- •Laravel Nova 5.7.6 (Silver Surfer)
- •Inertia.js 2.x
- •Vue 3.5.18 (Composition API)
- •HTML5 Canvas API
- •Laravel Mix 6.0.49
Core Components
PHP Field Class
Location: app/Nova/Fields/DamageAssessmentEditor.php
Key Responsibilities:
- •Define field component name (
public $component = 'damage-assessment-editor') - •Accept configuration methods (image, damageAssessments, readonly, etc.)
- •Serialize field data to JSON via
jsonSerialize()method - •Resolve callables when passing data to Vue components
- •Pass damage type and severity options from constants
Pattern:
php
class DamageAssessmentEditor extends Field
{
public $component = 'damage-assessment-editor';
protected $imageUrlCallback = null;
protected $staticImageUrl = null;
protected $imageUrls = null;
// Fluent configuration methods
public function image($url)
{
if (is_callable($url)) {
$this->imageUrlCallback = $url;
} else {
$this->staticImageUrl = $url;
}
return $this;
}
public function withImages($images)
{
$this->imageUrls = $images;
return $this;
}
// Serialize for Vue
public function jsonSerialize(): array
{
return array_merge(parent::jsonSerialize(), [
'imageUrl' => $this->resolveImageUrl(),
'imageUrls' => value($this->imageUrls),
'damageTypes' => $this->getDamageTypeOptions(),
'severityLevels' => $this->getSeverityLevelOptions(),
]);
}
protected function getDamageTypeOptions(): array
{
$options = [];
foreach (\App\Constants\DamageType::all() as $type) {
$options[] = [
'value' => $type,
'label' => \App\Constants\DamageType::label($type),
'color' => \App\Constants\DamageType::color($type),
];
}
return $options;
}
}
Vue Components
Location: resources/js/nova/fields/damage-assessment-editor/
Three Required Files:
- •IndexField.vue - Display in resource index table
- •DetailField.vue - Display on resource detail page
- •FormField.vue - Display on create/edit forms
Component Registration:
javascript
// resources/js/nova-components.js
import DamageAssessmentEditorIndexField from './nova/fields/damage-assessment-editor/IndexField.vue';
import DamageAssessmentEditorDetailField from './nova/fields/damage-assessment-editor/DetailField.vue';
import DamageAssessmentEditorFormField from './nova/fields/damage-assessment-editor/FormField.vue';
Nova.booting((app) => {
Nova.component('index-damage-assessment-editor', DamageAssessmentEditorIndexField);
Nova.component('detail-damage-assessment-editor', DamageAssessmentEditorDetailField);
Nova.component('form-damage-assessment-editor', DamageAssessmentEditorFormField);
});
Nova 5.x + Inertia 2.x Patterns
Inertia 2 Migration (CRITICAL)
Old (Inertia 1.x):
javascript
import { usePage } from '@inertiajs/inertia-vue3'
import { Inertia } from '@inertiajs/inertia'
New (Inertia 2.x):
javascript
import { router as Inertia, usePage } from '@inertiajs/vue3'
Error Handling
Old:
javascript
import { Errors } from 'form-backend-validation'
New:
javascript
import { Errors } from 'laravel-nova'
Vue 3 Composition API Pattern
vue
<script setup>
import { ref, computed, onMounted } from 'vue';
import { usePage } from '@inertiajs/vue3';
const props = defineProps({
field: Object,
resourceName: String,
resourceId: [String, Number],
});
const emit = defineEmits(['field-changed']);
// Reactive state
const selectedBox = ref(null);
const damageBoxes = ref([]);
// Computed properties
const damageTypes = computed(() => props.field.damageTypes || []);
const severityLevels = computed(() => props.field.severityLevels || []);
// Methods
const addDamageBox = (coordinates) => {
damageBoxes.value.push({
x: coordinates.x,
y: coordinates.y,
width: coordinates.width,
height: coordinates.height,
damage_type: null,
severity: null,
});
};
// Lifecycle
onMounted(() => {
initializeCanvas();
});
</script>
Field Data Flow
1. Nova Resource → PHP Field
php
// app/Nova/DamageAssessment.php
use App\Nova\Fields\DamageAssessmentEditor;
Panel::make('Visual Damage Editor', [
DamageAssessmentEditor::make('Visual Editor', 'damage_editor_demo')
->image(function () {
return $this->submissionImage?->file_url ?? asset('images/placeholder.svg');
})
->damageAssessments(function () {
return $this->submissionTradingCard?->damageAssessments ?? collect([]);
})
->readonly()
->onlyOnDetail(),
]),
2. PHP Field → Vue Component (via jsonSerialize)
php
public function jsonSerialize(): array
{
return array_merge(parent::jsonSerialize(), [
'imageUrl' => $this->resolveImageUrl(), // Resolved callable
'damageTypes' => $this->getDamageTypeOptions(),
'severityLevels' => $this->getSeverityLevelOptions(),
]);
}
3. Vue Component Receives Props
vue
<script setup>
const props = defineProps({
field: Object, // Contains imageUrl, damageTypes, severityLevels, etc.
resourceName: String,
resourceId: [String, Number],
});
// Access data
const imageUrl = computed(() => props.field.imageUrl);
const damageTypes = computed(() => props.field.damageTypes || []);
</script>
Canvas Implementation
Canvas Setup
vue
<template>
<div class="damage-assessment-editor">
<canvas
ref="canvasRef"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@click="selectBox"
/>
<div class="damage-boxes">
<div v-for="(box, index) in damageBoxes" :key="index">
<select v-model="box.damage_type">
<option v-for="type in damageTypes" :key="type.value" :value="type.value">
{{ type.label }}
</option>
</select>
<select v-model="box.severity">
<option v-for="level in severityLevels" :key="level.value" :value="level.value">
{{ level.label }}
</option>
</select>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
const props = defineProps({
field: Object,
});
const canvasRef = ref(null);
const damageBoxes = ref([]);
const isDrawing = ref(false);
const damageTypes = computed(() => props.field.damageTypes || []);
const severityLevels = computed(() => props.field.severityLevels || []);
const initializeCanvas = () => {
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
// Load image
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Draw existing boxes
drawAllBoxes();
};
img.src = props.field.imageUrl;
};
const drawAllBoxes = () => {
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
damageBoxes.value.forEach((box, index) => {
ctx.strokeStyle = index === selectedBox.value ? '#ff0000' : '#00ff00';
ctx.lineWidth = 2;
ctx.strokeRect(box.x, box.y, box.width, box.height);
});
};
onMounted(() => {
initializeCanvas();
});
</script>
Saving Data
Form Data Submission
vue
<script setup>
import { watch } from 'vue';
const props = defineProps({
field: Object,
});
const emit = defineEmits(['field-changed']);
// Watch for changes and emit to Nova
watch(damageBoxes, (newBoxes) => {
emit('field-changed', {
attribute: props.field.attribute,
value: JSON.stringify(newBoxes),
});
}, { deep: true });
</script>
PHP Field Hydration
php
protected function fillAttributeFromRequest(
NovaRequest $request,
$requestAttribute,
$model,
$attribute
) {
if ($request->exists($requestAttribute)) {
$value = $request[$requestAttribute];
// Decode JSON if needed
if (is_string($value)) {
$value = json_decode($value, true);
}
// Validate bounding boxes
$validated = $this->validateBoundingBoxes($value);
// Store in model
$model->{$attribute} = $validated;
}
}
Debugging Nova Custom Fields
Check Component Registration
bash
# Verify component is in compiled JS grep -o "detail-damage-assessment-editor" public/js/nova-components.js # Check file size (should be ~4MB+) ls -lh public/js/nova-components.js
Vue DevTools
bash
# Enable Vue DevTools for Nova ./vendor/bin/sail php vendor/bin/testbench nova:devtool enable-vue-devtool ./vendor/bin/sail npm run build
Console Debugging
vue
<script setup>
import { watch } from 'vue';
const props = defineProps({
field: Object,
});
// Debug field data
watch(() => props.field, (newField) => {
console.log('Field data:', newField);
console.log('Damage types:', newField.damageTypes);
console.log('Image URL:', newField.imageUrl);
}, { immediate: true, deep: true });
</script>
Common Issues
Issue 1: Component Not Rendering
- •Check
public $componentmatches registration name - •Verify component is registered in
nova-components.js - •Ensure assets are compiled (
npm run dev) - •Check browser console for errors
Issue 2: Data Not Passing to Vue
- •Verify
jsonSerialize()returns correct structure - •Check callables are resolved (use
value()helper) - •Ensure field is visible (
onlyOnDetail,canSee, etc.)
Issue 3: Canvas Not Drawing
- •Verify image URL is valid and accessible
- •Check canvas context is initialized
- •Ensure image loads before drawing
Issue 4: Type Errors with Inertia
- •Update to
@inertiajs/vue3v2 - •Replace
@inertiajs/inertiaimports withrouter - •Remove
form-backend-validationdependency
Testing Custom Fields
Manual Testing Checklist
- •✅ Field renders on detail page
- •✅ Image loads correctly
- •✅ Canvas is interactive (click, drag)
- •✅ Dropdowns show options from constants
- •✅ Damage types display with correct labels
- •✅ Severity levels show all options
- •✅ Boxes can be drawn and selected
- •✅ Delete key removes selected box
- •✅ Data saves to database correctly
- •✅ Page refresh preserves data
Browser Test
javascript
// In browser console Nova.$page.props.field // Should show field data with damageTypes, severityLevels, imageUrl
Best Practices
- •Always Resolve Callables - Use
value()helper or custom resolve methods - •Type Safety - Define proper return types in PHP and prop types in Vue
- •Reactive Data - Use Vue 3 refs and computed properties
- •Error Boundaries - Wrap canvas operations in try-catch
- •Asset Compilation - Always run
npm run devafter Vue changes - •Cache Clearing - Clear Laravel caches after PHP changes
- •Validation - Validate bounding box data server-side
- •Accessibility - Provide keyboard navigation for canvas
- •Performance - Debounce canvas drawing operations
- •Documentation - Comment complex canvas math
File Structure
code
app/Nova/Fields/ └── DamageAssessmentEditor.php # PHP field class resources/js/nova/fields/damage-assessment-editor/ ├── IndexField.vue # Table view ├── DetailField.vue # Detail page view (Canvas!) └── FormField.vue # Create/Edit form resources/js/ └── nova-components.js # Component registration public/js/ └── nova-components.js # Compiled bundle
Related Documentation
- •Nova Custom Fields
- •Inertia 2 Upgrade Guide
- •Vue 3 Composition API
- •HTML5 Canvas API
- •PCR Card:
app/Constants/DamageType.php- Damage type constants - •PCR Card:
app/Constants/DamageSeverity.php- Severity constants