Skip to content
Brok UI

Loading…

No results

Bulk Job

Open source

Async bulk-operation progress with a per-item failure list, retry-failed, download-report, and dismiss.

Version
v1.3.2
Stability
stable
License
MIT
Related
Progress
Button
Action Bar

Preview

Exporting 120 invoices
In progress
72 of 120 processed
60%
Archiving 40 orders
Finished
40 of 40 processed
100%
previews.components.bulk-job.default.blade.php Blade
{{-- One snapshot of a running job; the host re-renders it as the job progresses. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <x-ui.bulk-job :action="__('Exporting 120 invoices')" state="running" :done="72" :total="120" />
    <x-ui.bulk-job :action="__('Archiving 40 orders')" state="done" :done="40" :total="40" download-url="#" report-url="#" />
</div>

Installation

terminal
php artisan ui:add bulk-job

Note

This component ships an Alpine behavior module at resources/js/ui/bulk-job.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './bulk-job.js';

Registry contract

php artisan ui:add bulk-job writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/bulk-job.blade.php
Registry dependencies
progress button badge
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

bulk-job.md
# Brok UI: Bulk Job (`bulk-job`)

Async bulk-operation progress with a per-item failure list, retry-failed, download-report, and dismiss.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add bulk-job
```

## Usage

```blade
{{-- One snapshot of a running job; the host re-renders it as the job progresses. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <x-ui.bulk-job :action="__('Exporting 120 invoices')" state="running" :done="72" :total="120" />
    <x-ui.bulk-job :action="__('Archiving 40 orders')" state="done" :done="40" :total="40" download-url="#" report-url="#" />
</div>
```

## Props

- `action` (string, default ``) — Short description of what is running, e.g. "Archiving 40 orders".
- `state` (running|done, default `running`) — Whether the job is still in progress or has finished (with or without failures).
- `done` (int, default `0`) — Items processed so far. Clamped to [0, total].
- `total` (int, default `0`) — Total items in the run.
- `failed` (array, default `[]`) — Per-item failures: each entry is ['id', 'reason']. Drives the failure list and the retry-failed form.
- `message` (string|null, default `null`) — Optional inline warning or informational line under the progress sentence.
- `retryUrl` (string|null, default `null`) — POST action that resubmits only the failed item ids. Hidden when there are no failures.
- `downloadUrl` (string|null, default `null`) — Link to download files produced by the run. Shown once the job is no longer running.
- `reportUrl` (string|null, default `null`) — Link to download a report of the run. Shown once the job is no longer running.
- `dismissible` (bool, default `true`) — Whether a dismiss control is rendered. The dismissed state is local to the browser tab.
- `client` (bool, default `false`) — Emit bulk-job:retry with the failed ids instead of posting retryUrl. For a host that owns the job state.
- `retryEvent` (string, default `bulk-job:retry`) — Event the client-driven retry dispatches, with the failed ids as payload. Named by the caller.

## Use when

- Use to show state, status, notification, or lightweight social feedback near the relevant object.
- Reporting the outcome of a bulk operation (archive, export, re-price, re-tag, …) run across a selection.
- Naming every failure with its own reason so the operator can act on exactly what did not go through.

## Avoid when

- Do not overload small status elements with explanations that should remain visible in text.
- The operation is synchronous and finishes before the next paint; use inline validation or a toast instead.
- Nothing has failed and there is no per-item detail to report; a plain <x-ui.progress> is enough.

## Anti-patterns

- Using color or icon alone to communicate state

## Rules

- Use the `<brok:bulk-job>` tag (or `<x-ui.bulk-job>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/bulk-job
- Registry JSON (files, props, contract): https://brokui.dev/r/open/bulk-job.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

client.blade.php Blade
{{-- Client-state mode: retry is an event carrying the failed ids, not a POST. --}}
<div
    x-data="{ last: null }"
    x-on:bulk-job:retry="last = `retry ${$event.detail.ids.join(', ')}`"
    class="flex flex-col gap-3"
>
    <x-ui.bulk-job
        client
        :action="__('Archiving 40 orders')"
        state="done"
        :done="38"
        :total="40"
        :failed="[
            ['id' => '#1042', 'reason' => __('Already shipped')],
            ['id' => '#1043', 'reason' => __('Locked by another operator')],
        ]"
    />

    <p class="text-xs text-muted-foreground">
        {{ __('Host received:') }}
        <span data-slot="preview-event-log" class="font-mono text-foreground" x-text="last ?? '—'"></span>
    </p>
</div>
live.blade.php Blade
{{-- Slot-driven, client-state form: `done`/`total`/`failed` never exist as
     PHP props here — a consumer whose job lives entirely in Alpine state
     drives the sentence, the progress bar, and the failure rows straight off
     `job`, with no server re-render. Shown next to the ordinary array-prop
     form so both are visible side by side; "Simulate progress" advances the
     client-state job with no navigation. --}}
<div class="flex flex-col gap-6">
    <div>
        <p class="mb-1.5 text-xs font-medium text-muted-foreground">{{ __('Server-array form (props)') }}</p>
        <x-ui.bulk-job
            :action="__('Archiving 40 orders')"
            state="done"
            :done="38"
            :total="40"
            :failed="[
                ['id' => '#1042', 'reason' => __('Already shipped')],
                ['id' => '#1043', 'reason' => __('Locked by another operator')],
            ]"
        />
    </div>

    <div
        x-data="{
            job: { done: 12, total: 40, failed: [] },
            tick() {
                if (this.job.done >= this.job.total) return;
                this.job.done++;
                if (this.job.done === 27) {
                    this.job.failed.push({ id: '#1042', reason: @js(__('Already shipped')) });
                }
                if (this.job.done === 33) {
                    this.job.failed.push({ id: '#1043', reason: @js(__('Locked by another operator')) });
                }
            },
        }"
        class="flex flex-col gap-3"
    >
        <div>
            <p class="mb-1.5 text-xs font-medium text-muted-foreground">{{ __('Slot-driven form (client state)') }}</p>
            <x-ui.bulk-job :action="__('Archiving 40 orders')" state="running">
                <x-slot:summary>
                    <span
                        x-text="job.failed.length
                            ? `${job.done} {{ __('of') }} ${job.total} {{ __('processed') }}, ${job.failed.length} {{ __('failed') }}`
                            : `${job.done} {{ __('of') }} ${job.total} {{ __('processed') }}`"
                    ></span>
                </x-slot:summary>
                <x-slot:progress>
                    {{-- <x-ui.progress> computes its fill server-side from a
                         `value` prop, so it cannot be made reactive by
                         forwarding attributes onto its tag: the percentage
                         lives on a private child element the caller never
                         sees. A live bar reuses the primitive's own token
                         recipe directly instead, with real Alpine bindings on
                         elements the caller actually authors. --}}
                    <div
                        data-slot="progress"
                        role="progressbar"
                        aria-valuemin="0"
                        aria-valuemax="100"
                        aria-label="{{ __('Archiving 40 orders') }}"
                        x-bind:aria-valuenow="Math.round((job.done / job.total) * 100)"
                        data-state="determinate"
                        data-tone="default"
                        class="relative mt-2.5 h-2 w-full overflow-hidden rounded-full bg-muted"
                    >
                        <div
                            data-slot="progress-indicator"
                            class="h-full rounded-full bg-primary transition-[width] duration-300 ease-out motion-reduce:transition-none"
                            x-bind:style="`width: ${Math.round((job.done / job.total) * 100)}%`"
                        ></div>
                    </div>
                </x-slot:progress>
                <x-slot:failures>
                    <template x-if="job.failed.length">
                        <div data-slot="bulk-job-failures" class="mt-3 border-t border-border pt-3">
                            <div class="text-xs font-medium text-foreground">{{ __('Did not go through') }}</div>
                            <ul class="mt-1.5 space-y-1">
                                <template x-for="failure in job.failed" :key="failure.id">
                                    <li class="flex items-baseline gap-2 text-xs">
                                        <span class="font-mono text-info-text" x-text="failure.id"></span>
                                        <span class="min-w-0 truncate text-muted-foreground" x-text="failure.reason"></span>
                                    </li>
                                </template>
                            </ul>
                            <div class="mt-2.5">
                                <x-ui.button type="button" variant="outline" size="sm" x-on:click="job.failed = []">
                                    {{ __('Retry failed') }}
                                </x-ui.button>
                            </div>
                        </div>
                    </template>
                </x-slot:failures>
            </x-ui.bulk-job>
        </div>

        <x-ui.button type="button" size="sm" class="self-start" x-on:click="tick()">
            {{ __('Simulate progress tick') }}
        </x-ui.button>
    </div>
</div>
long-content.blade.php Blade
<div class="max-w-xs">
    <x-ui.bulk-job>
        {{ __('A deliberately long example that verifies wrapping, overflow, and content expansion without clipping important information.') }}
    </x-ui.bulk-job>
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
action string Short description of what is running, e.g. "Archiving 40 orders".
state running | done running Whether the job is still in progress or has finished (with or without failures).
done int 0 Items processed so far. Clamped to [0, total].
total int 0 Total items in the run.
failed array [] Per-item failures: each entry is ['id', 'reason']. Drives the failure list and the retry-failed form.
message string | null null Optional inline warning or informational line under the progress sentence.
retryUrl string | null null POST action that resubmits only the failed item ids. Hidden when there are no failures.
downloadUrl string | null null Link to download files produced by the run. Shown once the job is no longer running.
reportUrl string | null null Link to download a report of the run. Shown once the job is no longer running.
dismissible bool true Whether a dismiss control is rendered. The dismissed state is local to the browser tab.
client bool false Emit bulk-job:retry with the failed ids instead of posting retryUrl. For a host that owns the job state.
retryEvent string bulk-job:retry Event the client-driven retry dispatches, with the failed ids as payload. Named by the caller.

Slots

  • summary — Replaces the done/total/failed sentence. For a job whose counts live in client (Alpine) state rather than as PHP props — render your own x-text bound to that state instead of passing done/total/failed. Renders inside the component's role="status"/aria-live="polite" region, so x-text updates announce like any other change to the sentence.
  • progress — Replaces the whole <x-ui.progress> bar, since its fill percentage is exactly as live as the summary sentence. Compose <x-ui.progress> yourself with your own reactive binding, or render a different indicator entirely.
  • failures — Replaces the entire failure block — heading, rows, and retry action together, including the bulk-job-failures wrapper and its border/spacing. Takes priority over the failed prop when set. Only the caller's own x-if/x-show can know whether a client-side failures array is currently empty, so this slot owns that visibility too: pass nothing (or wrap it in your own x-if) to hide it, not a failed=[] prop.

Data slots

Stable hooks for CSS overrides and browser tests.

bulk-job bulk-job-dismiss bulk-job-failures bulk-job-glyph bulk-job-links bulk-job-message bulk-job-percent bulk-job-summary bulk-job-title

Behavior

  • Announces progress via role="status" and aria-live="polite", since a run usually finishes while the operator looks elsewhere.
  • The fill transition on the underlying progress bar is disabled under prefers-reduced-motion.
  • By default retry-failed and the download links are plain forms/links — no JavaScript is required to act on a finished job.
  • Dismiss hides the card locally via Alpine state; it does not clear the underlying job.
  • In client mode retry-failed becomes a button that dispatches bulk-job:retry with the failed ids, and needs no retryUrl; the download links stay links in both modes because they fetch a real file.
  • summary/progress/failures are additive slots for a job whose state lives in client (Alpine) code rather than PHP props: unset, the component renders exactly as before, straight from done/total/failed.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, authoredStateFixtures, responsive, rtl, darkMode, localized.

Guidance

Status and lightweight feedback

Communicate concise object or system state.

Use when

  • Use to show state, status, notification, or lightweight social feedback near the relevant object.
  • Reporting the outcome of a bulk operation (archive, export, re-price, re-tag, …) run across a selection.
  • Naming every failure with its own reason so the operator can act on exactly what did not go through.

Avoid when

  • Do not overload small status elements with explanations that should remain visible in text.
  • The operation is synchronous and finishes before the next paint; use inline validation or a toast instead.
  • Nothing has failed and there is no per-item detail to report; a plain <x-ui.progress> is enough.

Use instead

  • Persistent explanatory text

Anti-patterns

  • Using color or icon alone to communicate state
Anatomy
bulk-job bulk-job-title bulk-job-summary bulk-job-message bulk-job-dismiss bulk-job-failures bulk-job-links
Theming hooks
bulk-job bulk-job-title bulk-job-summary bulk-job-failures

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • Meet the WCAG 2.2 AA target declared in meta.a11y.
  • The progress bar's accessible name is derived from the action, not a bare percentage.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="bulk-job-{{ $record->id }}">
    {{-- One snapshot of a running job; the host re-renders it as the job progresses. --}}
    <div class="flex w-full max-w-md flex-col gap-4">
        <x-ui.bulk-job :action="__('Exporting 120 invoices')" state="running" :done="72" :total="120" />
        <x-ui.bulk-job :action="__('Archiving 40 orders')" state="done" :done="40" :total="40" download-url="#" report-url="#" />
    </div>
</div>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/bulk-job.blade.php Blade
{{--
    Bulk Job — what a bulk run actually did.

    "34 of 40 succeeded" is not a result anybody can act on. Every failure is
    named with its own reason, and retry-failed only resubmits those, so
    nobody has to re-run the whole batch and hope it goes better this time.

    `role="status"` + `aria-live="polite"`: a bulk run usually finishes while
    the operator is looking somewhere else. Sighted readers get the bar;
    without the live region nobody else is told at all. Polite, because a run
    can report progress every second and must never interrupt.

    Retrying has two state models, matching the rest of the toolbar set. By
    default `retryUrl` posts a real form carrying the failed ids, so a retry
    survives a page with no JavaScript. Pass `client` when the host owns the
    job's state: the retry button dispatches `bulk-job:retry` with those ids
    instead, and no URL is needed. The two download links stay links in both
    modes — they fetch a real file, which is a navigation, not state.

    `done`/`total`/`failed` are PHP values snapshotted once at render time —
    fine when a server re-renders the page, useless when the job lives in
    Alpine state on the client and changes every tick with no navigation to
    hang a re-render on. An Alpine *expression* prop (`doneExpression="job.done"`)
    would be a string of someone else's code smuggled through a PHP prop —
    unvalidatable, unescapable, and it still couldn't reach the retry button's
    payload. So the live parts take slots instead: `summary` (the done/total/
    failed sentence), `progress` (the whole `<x-ui.progress>` bar, since its
    fill percentage is exactly as live as the sentence), and `failures` (the
    *entire* failure block — heading, rows, and retry action together, because
    only the caller's own `x-if`/`x-show` can know whether the client-side
    array is empty right now). Slot content still renders inside this
    component's `role="status"`/`aria-live="polite"` region, so `x-text`
    updates and `x-for` row insertions inside a slot announce exactly like a
    Livewire-rerendered sentence would — no wiring needed on either side,
    because `aria-live` watches its whole subtree, not just the props this
    component itself renders. Leave all three unset and it renders exactly as
    before, straight from `done`/`total`/`failed`.

    Generalised from `orders/bulk-job.blade.php` and `customers/bulk-job.blade.php`
    (identical across ~19 admin modules bar the copy). The domain-specific i18n
    keys and the page's own polling/store wiring are left out — this renders
    one snapshot of job state from props; the host re-renders it as the job
    progresses (Livewire polling, a dispatched update, or a page refresh).
--}}
@props([
    // Short description of what is running, e.g. "Archiving 40 orders".
    'action' => '',
    // running|done — done covers both a clean finish and one with failures.
    'state' => 'running',
    'done' => 0,
    'total' => 0,
    // [['id' => '#1042', 'reason' => 'Already shipped'], ...]
    'failed' => [],
    // Optional inline warning/info line under the progress sentence.
    'message' => null,
    // Form action that resubmits only the failed items. Hidden with no failures.
    'retryUrl' => null,
    'downloadUrl' => null,
    'reportUrl' => null,
    'dismissible' => true,
    // Emit `bulk-job:retry` instead of posting `retryUrl`, for a host that
    // owns the job's state.
    'client' => false,
    // The event the client-driven retry dispatches. Named by the caller for the
    // same reason record-search's is: our vocabulary should not be imposed on a
    // host that already has one.
    'retryEvent' => 'bulk-job:retry',
])

@php
    $failed = array_values(array_filter((array) $failed, 'is_array'));
    $client = (bool) $client;
    $total = max(0, (int) $total);
    $done = max(0, min((int) $done, $total));
    $pct = $total > 0 ? (int) round($done / $total * 100) : 0;
    $running = $state === 'running';
    $tone = $failed !== [] ? 'warning' : ($running ? 'default' : 'success');
    $sentence = $failed !== []
        ? __(':done of :total processed, :failed failed', ['done' => $done, 'total' => $total, 'failed' => count($failed)])
        : __(':done of :total processed', ['done' => $done, 'total' => $total]);
    $statusLabel = $running ? __('In progress') : ($failed !== [] ? __('Finished with errors') : __('Finished'));
    $statusVariant = $running ? 'secondary' : ($failed !== [] ? 'soft-warning' : 'soft-success');
    // Status glyph: a spinner while running, a check when clean, a warning triangle with failures.
    $glyphWrap = match ($tone) {
        'success' => 'bg-success-soft text-success-text',
        'warning' => 'bg-warning-soft text-warning-text',
        default => 'bg-muted text-muted-foreground',
    };
@endphp

<div
    @if ($dismissible) x-data="{ dismissed: false }" x-show="!dismissed" x-cloak
    @elseif ($client) x-data="{}" @endif
    data-slot="bulk-job"
    data-state="{{ $state }}"
    data-tone="{{ $tone }}"
    role="status"
    aria-live="polite"
    {{ $attributes->merge(['class' => 'rounded-lg border border-border bg-card p-6 text-card-foreground']) }}
>
    <div class="flex items-start gap-4">
        <span data-slot="bulk-job-glyph" class="inline-flex size-8 shrink-0 items-center justify-center rounded-full {{ $glyphWrap }}" aria-hidden="true">
            @if ($running)
                <svg class="size-4 motion-safe:animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6" /></svg>
            @elseif ($failed !== [])
                <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3" /><path d="M12 9v4M12 17h.01" /></svg>
            @else
                <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7" /></svg>
            @endif
        </span>

        <div class="min-w-0 flex-1">
            <div class="flex flex-wrap items-center gap-2">
                <div data-slot="bulk-job-title" class="truncate text-sm font-semibold text-foreground">{{ $action }}</div>
                <x-ui.badge :variant="$statusVariant" size="sm">{{ $statusLabel }}</x-ui.badge>
            </div>
            <div data-slot="bulk-job-summary" class="mt-1 text-sm text-muted-foreground">
@isset($summary)
                {{ $summary }}
@else
                {{ $sentence }}
@endisset
            </div>
            @if ($message)
                <div data-slot="bulk-job-message" class="mt-1 text-sm text-warning-text">{{ $message }}</div>
            @endif
        </div>

        @if ($dismissible)
            <button
                type="button"
                data-slot="bulk-job-dismiss"
                x-on:click="dismissed = true"
                aria-label="{{ __('Dismiss') }}"
                class="-me-2 -mt-2 inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
            >
                <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
            </button>
        @endif
    </div>

    <div class="mt-4 flex items-center gap-4">
@isset($progress)
        <div class="min-w-0 flex-1">{{ $progress }}</div>
@else
        <x-ui.progress
            :value="$pct"
            :tone="$tone"
            :label="$action !== '' ? $action : __('Bulk job progress')"
            class="min-w-0 flex-1"
        />
        <span data-slot="bulk-job-percent" class="w-10 shrink-0 text-end text-xs font-medium tabular-nums text-muted-foreground">{{ $pct }}%</span>
@endisset
    </div>

    @if ($failed !== [] && ! isset($failures))
        <div data-slot="bulk-job-failures" class="mt-4 rounded-md border border-border bg-muted/50 p-4">
            <div class="flex items-center justify-between gap-2">
                <div class="text-sm font-medium text-foreground">{{ __('Did not go through') }}</div>
                <span class="text-xs tabular-nums text-muted-foreground">{{ count($failed) }}</span>
            </div>
            <ul class="mt-2 divide-y divide-border">
                @foreach ($failed as $failure)
                    <li class="flex items-baseline gap-3 py-2 text-sm">
                        <span class="relative top-px inline-block size-2 shrink-0 rounded-full bg-destructive" aria-hidden="true"></span>
                        <span class="shrink-0 font-mono text-foreground">{{ $failure['id'] ?? '' }}</span>
                        <span class="min-w-0 truncate text-muted-foreground">{{ $failure['reason'] ?? '' }}</span>
                    </li>
                @endforeach
            </ul>

            @if ($client)
                <div class="mt-2">
                    <x-ui.button
                        type="button"
                        variant="outline"
                        size="sm"
                        x-on:click="$dispatch({{ Illuminate\Support\Js::from($retryEvent) }}, {{ Illuminate\Support\Js::from(['ids' => array_map(static fn (array $failure): string => (string) ($failure['id'] ?? ''), $failed)]) }})"
                    >
                        {{ __('Retry :count failed', ['count' => count($failed)]) }}
                    </x-ui.button>
                </div>
            @elseif ($retryUrl)
                <form method="POST" action="{{ $retryUrl }}" class="mt-2">
                    @csrf
                    @foreach ($failed as $failure)
                        <input type="hidden" name="ids[]" value="{{ $failure['id'] ?? '' }}" />
                    @endforeach
                    <x-ui.button type="submit" variant="outline" size="sm">
                        {{ __('Retry :count failed', ['count' => count($failed)]) }}
                    </x-ui.button>
                </form>
            @endif
        </div>
    @endif
@isset($failures)
    {{ $failures }}
@endisset

    @if (! $running && ($downloadUrl || $reportUrl))
        <div data-slot="bulk-job-links" class="mt-4 flex flex-wrap gap-2">
            @if ($downloadUrl)
                <x-ui.button as="a" href="{{ $downloadUrl }}" variant="outline" size="sm">
                    <x-slot:icon><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /></svg></x-slot:icon>
                    {{ __('Download files') }}
                </x-ui.button>
            @endif
            @if ($reportUrl)
                <x-ui.button as="a" href="{{ $reportUrl }}" variant="outline" size="sm">
                    <x-slot:icon><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M8 13h8M8 17h5" /></svg></x-slot:icon>
                    {{ __('Download report') }}
                </x-ui.button>
            @endif
        </div>
    @endif
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.3.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0