tunezilla/dynamic-action-fields
Laravel package for defining dynamic, configurable fields on actions, letting you build and render action forms at runtime instead of hardcoding inputs. Useful for customizable admin workflows, reusable action classes, and flexible user-defined input handling.
Install and pin. composer require tunezilla/dynamic-action-fields, then immediately open composer.json and tighten the constraint to the exact version you tested. Nova is paid and closed-source; this package is effectively single-maintainer with zero dependents, so you are the canary. Record the Nova version alongside the package version in the PR description — treat them as one dependency.
Read the trait before you write any code. The whole integration surface is small (an afternoon's read). Open the package's src/ first and answer four questions in writing before touching an action:
fields() moves into.rules()) flows through the same dynamic path as rendering.$this->resource, so you can seed defaults from the checked rows.First use case: one low-risk action, conditionally. Pick an action where a type or reason_code select should surface one extra field and hide several irrelevant ones. Keep the existing static fields() implementation intact behind the trait so rollback is a one-line revert. Do not refactor a fleet of actions in the first pass — the point of the first conversion is a real go/no-go gate.
Write the smoke test in the same PR. A test that opens the action modal as an authorized user and asserts the returned field set is the highest-leverage artifact in the entire integration. Nova's closed source means this test is your only reliable compatibility signal on a Nova bump.
Trait-on-action, fields-into-hook. The mental model is "move fields() into a callback that re-runs against the live request." Existing field objects (Select::make(...), Text::make(...)) stay as they are — you are changing when the array is built, not what's in it. Verify the exact names against source; the shape is roughly:
use Tunezilla\DynamicActionFields\Concerns\ResolvesDynamicFields; // verify namespace
class IssueRefund extends Action
{
use ResolvesDynamicFields;
public function fields(NovaRequest $request): array
{
// called each time the modal is serialized
return [
Select::make('Method', 'method')->options([...]),
// conditional branch off a sibling value / selected rows
];
}
}
Branch on the three useful contexts. In practice the branching inputs are: the selected resources ($this->resources / the models themselves), the acting user ($request->user() — role, tenant, permissions), and record state (order status, currency, whether the row is already refunded). Reach for these directly in the resolver rather than encoding variants as separate action classes. That consolidation — one action instead of five near-duplicates — is the actual day-to-day payoff, not conditional rendering for its own sake.
Dependent selects. For a country → region or category → subcategory pair, resolve the child's options() from the parent's value in the same pass. Confirm whether that works at modal-open time only (parent pre-selected from the resource) or live (parent changes inside the modal). If it's open-time only, design around it: seed the parent from the selected rows so the dependent field is correct on first paint, and accept that in-modal edits won't cascade.
Keep validation co-located with rendering. Derive rules() from the same condition function as fields(). The clean pattern is a private method that returns a context object or a matched variant key, consumed by both:
private function variant(NovaRequest $request): string { /* ... */ }
public function fields(NovaRequest $request): array { return match($this->variant($request)) { ... }; }
public function rules(NovaRequest $request): array { return match($this->variant($request)) { ... }; }
One source of truth for "which variant are we in" is what prevents required-but-hidden fields and, worse, values that pass unvalidated on a data-mutating admin action.
Coexistence and composition. Check whether dynamic fields double-serialize when combined with Nova's native dependsOn / withMeta. Pick one mechanism per action rather than mixing them — mixed strategies are the source of "the modal shows the field but the payload doesn't contain it" bugs. Same discipline for withMeta(): if you attach meta conditionally, make sure the meta resolver runs in the same pass as the fields.
One base class per team. Put the trait on a single project-owned abstract DynamicAction base class rather than sprinkling use across action classes. Then a swap to a vendored fork, an inline copy, or a hand-rolled replacement is mechanical instead of a hunt. Keep a short inventory file (action class → what it resolves against → why) in the repo; it doubles as the migration checklist for a Nova upgrade and the onboarding doc.
$this->resources->load('customer.tenant') inside the resolver, called repeatedly, is a fast path to a slow modal. Resolve once into a local property and branch off that.Select and returning it works in dev and produces baffling stale-options bugs across requests.Log::debug() the resolved field names plus the branch key inside the hook; you'll see immediately whether the resolver saw what you expected or whether Nova never called it. If the hook doesn't fire at all, suspect the trait isn't applied (or the action isn't using the base class) — not Nova.dependsOn, or two separate actions. For a single edge case, the maintenance tax exceeds the gain.How can I help you explore Laravel packages today?