Split NPC page: read-only detail + game notes, separate full edit form
- /npcs/{id} (app_npc_show): portrait, all fields shown read-only, and
the only editable control is the game-notes textarea. Father/Mother
link through to their own pages.
- /npcs/{id}/edit (app_npc_edit): the full structured form, now WITHOUT
game notes; saving returns to the detail page.
- NpcType drops gameNotes; new NpcGameNotesType holds just that field.
- The DataTable row link (/npcs/{id}) now lands on the detail page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Npc;
|
||||
use App\Form\NpcGameNotesType;
|
||||
use App\Form\NpcType;
|
||||
use App\Repository\NpcRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -65,7 +66,32 @@ class NpcController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/npcs/{id}', name: 'app_npc_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
|
||||
/**
|
||||
* In-play detail page: everything read-only, plus the game-notes field.
|
||||
*/
|
||||
#[Route('/npcs/{id}', name: 'app_npc_show', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
|
||||
public function show(Request $request, Npc $npc, EntityManagerInterface $em): Response
|
||||
{
|
||||
$form = $this->createForm(NpcGameNotesType::class, $npc);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'Game notes saved.');
|
||||
|
||||
return $this->redirectToRoute('app_npc_show', ['id' => $npc->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('npc/show.html.twig', [
|
||||
'npc' => $npc,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full edit of the NPC's structured fields (no game notes).
|
||||
*/
|
||||
#[Route('/npcs/{id}/edit', name: 'app_npc_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
|
||||
public function edit(Request $request, Npc $npc, EntityManagerInterface $em): Response
|
||||
{
|
||||
$form = $this->createForm(NpcType::class, $npc);
|
||||
@@ -75,7 +101,7 @@ class NpcController extends AbstractController
|
||||
$em->flush();
|
||||
$this->addFlash('success', sprintf('Saved %s.', $npc->getName()));
|
||||
|
||||
return $this->redirectToRoute('app_npc_edit', ['id' => $npc->getId()]);
|
||||
return $this->redirectToRoute('app_npc_show', ['id' => $npc->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('npc/edit.html.twig', [
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Npc;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* The only editable thing on the NPC detail page: the DM's live play notes.
|
||||
*/
|
||||
class NpcGameNotesType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('gameNotes', TextareaType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'attr' => ['rows' => 14, 'placeholder' => 'What happened with this NPC during play…'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['data_class' => Npc::class]);
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,8 @@ class NpcType extends AbstractType
|
||||
->add('mother', EntityType::class, $parent)
|
||||
->add('villageStatus', EnumType::class, $enum(VillageStatus::class))
|
||||
->add('bloodCartInvolvement', EnumType::class, $enum(BloodCartInvolvement::class))
|
||||
->add('mysteryImportance', EnumType::class, $enum(MysteryImportance::class))
|
||||
->add('gameNotes', TextareaType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Game notes',
|
||||
'attr' => ['rows' => 10, 'placeholder' => 'What happened with this NPC during play…'],
|
||||
]);
|
||||
->add('mysteryImportance', EnumType::class, $enum(MysteryImportance::class));
|
||||
// gameNotes lives on the detail page, not this form.
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}{{ npc.name }} · NPC's · dmtools{% endblock %}
|
||||
{% block title %}Edit {{ npc.name }} · NPC's · dmtools{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-start mb-3 gap-3">
|
||||
@@ -10,53 +10,38 @@
|
||||
class="rounded border object-fit-cover flex-shrink-0"
|
||||
onerror="this.remove()">
|
||||
<div>
|
||||
<a href="{{ path('app_npcs') }}" class="text-decoration-none small">← All NPC's</a>
|
||||
<h1 class="h3 mb-0">{{ npc.name }}</h1>
|
||||
<span class="text-body-secondary">{{ npc.family }}{% if npc.occupation %} · {{ npc.occupation }}{% endif %}</span>
|
||||
<a href="{{ path('app_npc_show', {id: npc.id}) }}" class="text-decoration-none small">← Back to {{ npc.name }}</a>
|
||||
<h1 class="h3 mb-0">Edit {{ npc.name }}</h1>
|
||||
<span class="text-body-secondary">Structured details — game notes live on the NPC page.</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if npc.updatedAt %}
|
||||
<span class="text-body-secondary small">Updated {{ npc.updatedAt|date('Y-m-d H:i') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-8">{{ form_row(form.name) }}</div>
|
||||
<div class="col-4">{{ form_row(form.age) }}</div>
|
||||
</div>
|
||||
{{ form_row(form.family) }}
|
||||
{{ form_row(form.race) }}
|
||||
{{ form_row(form.occupation) }}
|
||||
{{ form_row(form.villageStatus) }}
|
||||
{{ form_row(form.bloodCartInvolvement) }}
|
||||
{{ form_row(form.mysteryImportance) }}
|
||||
<div class="row">
|
||||
<div class="col-6">{{ form_row(form.father) }}</div>
|
||||
<div class="col-6">{{ form_row(form.mother) }}</div>
|
||||
</div>
|
||||
{{ form_row(form.relationships) }}
|
||||
</div>
|
||||
<div class="card shadow-sm" style="max-width: 40rem;">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-8">{{ form_row(form.name) }}</div>
|
||||
<div class="col-4">{{ form_row(form.age) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">Game notes</div>
|
||||
<div class="card-body">
|
||||
{{ form_row(form.gameNotes, {label: false}) }}
|
||||
</div>
|
||||
{{ form_row(form.family) }}
|
||||
{{ form_row(form.race) }}
|
||||
{{ form_row(form.occupation) }}
|
||||
{{ form_row(form.villageStatus) }}
|
||||
{{ form_row(form.bloodCartInvolvement) }}
|
||||
{{ form_row(form.mysteryImportance) }}
|
||||
<div class="row">
|
||||
<div class="col-6">{{ form_row(form.father) }}</div>
|
||||
<div class="col-6">{{ form_row(form.mother) }}</div>
|
||||
</div>
|
||||
{{ form_row(form.relationships) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
<a href="{{ path('app_npcs') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
<a href="{{ path('app_npc_show', {id: npc.id}) }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}{{ npc.name }} · NPC's · dmtools{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-start mb-3 gap-3">
|
||||
<div class="d-flex gap-3">
|
||||
<img src="{{ asset(npc.portraitPath) }}" alt="Portrait of {{ npc.name }}"
|
||||
width="110" height="140"
|
||||
class="rounded border object-fit-cover flex-shrink-0"
|
||||
onerror="this.remove()">
|
||||
<div>
|
||||
<a href="{{ path('app_npcs') }}" class="text-decoration-none small">← All NPC's</a>
|
||||
<h1 class="h3 mb-0">{{ npc.name }}</h1>
|
||||
<span class="text-body-secondary">{{ npc.family }}{% if npc.occupation %} · {{ npc.occupation }}{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ path('app_npc_edit', {id: npc.id}) }}" class="btn btn-outline-secondary btn-sm">Edit details</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">Details</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-5">Family</dt><dd class="col-7">{{ npc.family }}</dd>
|
||||
<dt class="col-5">Age</dt><dd class="col-7">{{ npc.age ?? '—' }}</dd>
|
||||
<dt class="col-5">Race</dt><dd class="col-7">{{ npc.race.value }}</dd>
|
||||
<dt class="col-5">Occupation</dt><dd class="col-7">{{ npc.occupation ?? '—' }}</dd>
|
||||
<dt class="col-5">Village status</dt><dd class="col-7">{{ npc.villageStatus.value }}</dd>
|
||||
<dt class="col-5">Blood cart</dt><dd class="col-7">{{ npc.bloodCartInvolvement.value }}</dd>
|
||||
<dt class="col-5">Mystery importance</dt><dd class="col-7">{{ npc.mysteryImportance.value }}</dd>
|
||||
<dt class="col-5">Father</dt>
|
||||
<dd class="col-7">
|
||||
{% if npc.father %}<a href="{{ path('app_npc_show', {id: npc.father.id}) }}">{{ npc.father.name }}</a>{% else %}—{% endif %}
|
||||
</dd>
|
||||
<dt class="col-5">Mother</dt>
|
||||
<dd class="col-7">
|
||||
{% if npc.mother %}<a href="{{ path('app_npc_show', {id: npc.mother.id}) }}">{{ npc.mother.name }}</a>{% else %}—{% endif %}
|
||||
</dd>
|
||||
{% if npc.relationships %}
|
||||
<dt class="col-5">Relationships / Notes</dt>
|
||||
<dd class="col-7 mb-0">{{ npc.relationships|nl2br }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Game notes</span>
|
||||
{% if npc.updatedAt %}
|
||||
<span class="text-body-secondary small fw-normal">saved {{ npc.updatedAt|date('Y-m-d H:i') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.gameNotes) }}
|
||||
<button class="btn btn-primary mt-2">Save notes</button>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user