Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Progress Field Laravel Package

masoudi/nova-progress-field

A Laravel Nova field for displaying progress bars directly in your resource forms and detail views. Render percentage-based progress with a clean, customizable bar, ideal for tracking completion, goals, or any numeric value inside Nova.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements. Laravel Nova plus the usual asset pipeline. Given the last release (Aug 2022) the package almost certainly declares laravel/nova: ^4.0 in composer.json — that constraint, not the PHP code, is what will bite you on Nova 5.

Install.

composer require masoudi/nova-progress-field

If Composer refuses to resolve against your Nova version, do not reach for --ignore-platform-reqs — that flag doesn't bypass package-level constraints. Fork the repo, widen the laravel/nova constraint in the fork's composer.json, and require your fork via a VCS repository entry.

Where to look first, in this order:

  1. src/ — the field class. In a package this size it is 50–150 lines and is the documentation.
  2. src/FieldServiceProvider.php (or ProgressFieldServiceProvider.php) — confirm Nova::script('nova-progress-field', __DIR__.'/../dist/js/field.js') is registered in boot(). If it isn't, the field will render as an empty cell and you'll waste an hour.
  3. resources/js/components/Field.vue — the actual bar. This tells you which withMeta options the component reads (field.min, field.max, field.color, etc.). Don't trust the README for the option names; read props/field.* references in the component.
  4. dist/js/field.js — confirm it's committed and current relative to the .vue source.

First use case. The canonical one is a percentage column on an index table: an accessor that returns a 0–100 integer.

// Model
public function getCompletionAttribute(): int
{
    $total = max(1, $this->tasks_count);
    return (int) round(($this->done_tasks_count / $total) * 100);
}

// Resource
ProgressField::make('Completion'),

Ship that, eyeball the index, and only then start layering options — the value of a package like this is that the failure modes are all in the asset layer, and you want to rule those out on a one-field commit.

Implementation Patterns

Keep the math in the model, keep the field dumb. Progress fields expect a raw number. Formatting ("%d%%", suffixes, number_format) belongs in the accessor or in the component — a string value is the single most common cause of a zero-width bar.

Avoid the N+1 on index. This is the pattern that actually matters day-to-day. Twenty-five rows each running two aggregates is a 50-query index page:

// Resource::indexQuery()
public static function indexQuery(NovaRequest $request, $query)
{
    return $query->withCount([
        'tasks',
        'tasks as done_tasks_count' => fn ($q) => $q->where('done', true),
    ]);
}

Then read $resource->tasks_count in the accessor without touching the DB.

Compute on the fly with resolveUsing. For values that are too expensive or too contextual to be a column, resolve in the field and skip the accessor:

ProgressField::make('Completion')
    ->resolveUsing(fn ($value, $resource) =>
        (int) round($resource->done_tasks_count / max(1, $resource->tasks_count) * 100))
    ->onlyOnIndex(),

Note resolveUsing runs per-row; pair it with indexQuery above or you've just moved the N+1.

Split index and detail. The bar is great for scanning, bad for auditing. Show the bar on index and a plain Number::make('Completion')->displayUsing(fn ($v) => $v.'%') on detail, so anyone who needs the exact figure can get it and sort by it.

Sorting. ->sortable() on an accessor does nothing useful unless you provide a callback:

ProgressField::make('Completion')->sortable(function ($request, $query, $direction) {
    $query->orderByRaw(
        '(SELECT COUNT(*) FROM tasks WHERE tasks.project_id = projects.id AND tasks.done = 1) '
        .'/(GREATEST((SELECT COUNT(*) FROM tasks WHERE tasks.project_id = projects.id), 1)) '
        .$direction
    );
}),

Pass options through withMeta. If the component reads field.min / field.max / field.color, this is the cheapest extension point — no fork, no rebuild:

ProgressField::make('Completion')->withMeta(['max' => 100, 'color' => '#22c55e']),

Verify the exact keys against the .vue file before you trust them; there's no schema here.

Forking the Vue component is the real extension point once withMeta runs out. Typical reason: colour that varies with value (red under 30%, amber under 70%). Add a computed: { colorFor () { ... } } to the component and reference it in the inline style. Copy resources/js/components/Field.vue to your own nova-components/ rather than editing vendor/, or your next composer install silently reverts the change.

Actions and refresh. Nova refreshes the index after an action completes, so a progress bar that "doesn't update" is nearly always a cached accessor or a stale withCount on the query — not the field.

Gotchas and Tips

It's archived with one star and zero dependents. That's the headline risk. There are no security patches, no Nova 5 support, and no issue triage. The pragmatic stance for most teams: treat the source as copy-paste material, not a dependency. A Nova field class plus a <div class="progress"><div :style="{width: value + '%'}"></div></div> component is roughly 60 lines and removes the supply-chain and upgrade risk entirely. Budget an hour.

The blank-cell failure. If the column renders empty or shows the raw value, the compiled JS isn't loaded. Check three things in order: Nova::script(...) is registered in boot(); dist/js/field.js exists and is committed; you've hard-refreshed (Cmd+Shift+R) — Nova's asset hash doesn't always bust. php artisan nova:publish --force plus a browser cache clear resolves the majority of reports.

Value shape. Cast to int and clamp in the accessor: max(0, min(100, $value)). A float, a string, or a value of 110 all produce either a NaN width or an overflowing bar, and neither throws — you just get a wrong-looking UI.

Division by zero on resources with no children is the classic 500 on an otherwise-empty index page. Always max(1, $total).

Dark mode. Small packages like this commonly hardcode hex colours or use a handful of utility classes, so they won't inherit Nova's CSS variables and will look wrong on a dark theme. Check for literal #fff / #eee in the component before shipping to a team that uses dark mode.

Constraint mismatch is a hard wall. If Composer says the package requires laravel/nova ^4.0 and you're on 5, no flag helps — fork and edit composer.json. Do this at the start, not after you've wired the field into five resources.

Debugging recipe. ->resolveUsing(function ($v) { dd($v); }) first. If the value is right and the bar is wrong, the bug is in field.js — stop reading PHP. If dd never fires, the field isn't in the resource's fields() for that view (check onlyOnIndex / showOnIndex).

A Text field with asHtml() is often enough. <progress value="72" max="100"> is native HTML, styled by the browser, needs no JS asset, and survives Nova upgrades. For a single low-traffic admin panel, this beats maintaining a fork of an archived package — worth trying before you invest in the Vue component.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle