Skip to content

Unified all modals into blade components - #19610

Merged
snipe merged 11 commits into
developfrom
modal-components-unification
Sep 1, 2026
Merged

Unified all modals into blade components#19610
snipe merged 11 commits into
developfrom
modal-components-unification

Conversation

@snipe

@snipe snipe commented Sep 1, 2026

Copy link
Copy Markdown
Member

This moves the modals from resources/views/modals/* into the blade components directory and updates them to use the form blade components.

While I was testing this, I also realized that inline comments in the props break the IDE view, making it look like there's a parse error when there isn't one, so I moved those all to the top of the file in a blade comment. Shouldn't have been necessary, but at least it doesn't look borked now.

I also refactored the form-row blade component, because in some modals, we use a "vertical" form (BS parlance) and in others we use a horizontal form. For reference, all of our edit/create forms are vertical - it's what Bootstrap defaults to, and is generally better for larger screens. That's the model where you have a label and then to the right, the input field.

Horizontal forms are only used in a few modals (though that might change - they do look better in modals) where the label is stacked on TOP of the input field, like the replenish modal:

Screenshot 2026-09-01 at 2 48 03 PM

So the form-row blade will work as expected as-is, but now pulls in the field logic (date picker, text, textarea, etc) into its own blade component, just to keep the form-row blade a bit clearer. Now it only handles the layout, and the new field component handles which field type needs to be slotted in.

I once again ran into the weird ID problem where clicking on a "new" modal button would trash the company select2 on parent page. That's fixed now by adding the modal ID so that select2 has its own modal ID that doesn't step on the other select lists on the page.

(Sorry for all the inline-comment-moving noise in this one - seemed like a good opportunity to fix it, but it probably should have been in another PR.)

There are THREE categories of modal (and two styles):

AJAX modal (loaded into #createModal via "New" buttons)

Used when a -select picker's "New" button opens a modal to create a resource on the fly and inject the saved value into the corresponding select2 that exists on the parent page.

{{-- resources/views/blade/modals/blahblah.blade.php --}}
<x-modals
    :title="trans('admin/blahblah/table.create')"
    :action="route('api.blahblah.store')"
    submitToSelect2
>
    <x-form.row name="name" :label="trans('general.name')" required />
</x-modals>

This is rendered by ModalController::show when the trigger points at route('modal.show', 'blahblah'). The API endpoint's JSON response must include payload.id and payload.name so the JS can push a new option into the trigger select2.

Nested -select widgets auto-hide their own "New" buttons (via @aware) since #createModal is single-slot (and we wouldn't want to open a modal on top of another modal.) Pass id="modal_<name>_select" on any -select to avoid a duplicate id if the parent page also renders that picker.

Standalone modal (inline on the page)

Used for uploads and inline forms that submit natively. Existing examples: adjust-quantity, request-item, maintenance-complete, add-note, upload-file, confirm-action.

{{-- somewhere on the host page, usually the bottom of the blade --}}
<x-modals id="fooModal" :title="trans('general.foo')" :action="route('foo.store')">
    <x-form.row name="bar" :label="trans('general.bar')" required />
</x-modals>

{{-- trigger --}}
<a data-toggle="modal" data-target="#fooModal" class="btn btn-primary">Open</a>

Global confirm modal (delete / restore)

This has existed for a long time, but it bears repeating here: don't roll your own confirm dialog. layouts/default.blade.php renders #dataConfirmModal (delete) and #restoreConfirmModal (restore) once for the whole app. Attributes on .delete-asset and .restore-asset in snipeit.js populate the modal's form action from the trigger's data-href / href and open it.

You can create these just by giving it the right class + data attributes. No blade needed.

<a
    href="{{ route('foo.destroy', $item) }}"
    class="btn btn-danger delete-asset"
    data-toggle="modal"
    data-title="{{ trans('general.delete') }}"
    data-content="{{ trans('general.delete_confirm', ['item' => $item->name]) }}"
    data-icon="fa-exclamation-triangle"
>Delete</a>

Same for restore, use restore-asset class.

Note: We would normally not use an <a> for changing data - that should be a POST request, not a GET - but because there is another layer, the confirmation modal, it's okay. Maybe.

Layout

Horizontal (BS3 label-left) is the default. Pass stacked on <x-modals> for a vertical form. Rows inside inherit it automatically.

<x-modals :title="..." :action="..." stacked>
    <x-form.row stacked name="note" type="textarea" :label="trans('general.notes')" />
</x-modals>

Props:

Prop Type Default What it does
id string null DOM id on the outer .modal wrapper. Omit for AJAX modals (they land inside the shared #createModal). Required for standalone modals so their trigger's data-target="#..." finds them.
title string null Renders as .modal-title in the header. Skip if you supply <x-slot:header>.
action string null Form action URL. Standalone modals need it up front. AJAX modals whose action URL is populated per-click by JS (e.g. adjust-quantity, request-item) can omit it.
submitToSelect2 bool false Flips the modal into AJAX mode: form lives inside .modal-body, save button is #modal-save outside the form, and the delegated jQuery handler POSTs the form and pushes the created row into the trigger <select>.
stacked bool false Layout switch. Default = form-horizontal (BS3 label-left grid, matches every regular edit/create form). stacked = vertical form (labels above inputs). Must be paired with <x-form.row stacked> on each row inside.
submit_label string trans('general.save') Text on the primary save/confirm button.
submit_class string 'btn-primary' Bootstrap class on the save button. Common overrides: btn-danger for destructive confirms, btn-success for state advancement, btn-theme for uploads.
form_class string null Extra classes layered onto the <form> element on top of the layout default. Rarely needed.
form_attrs string '' Raw HTML attribute string spliced onto the <form>. Use for enctype="multipart/form-data", extra ids that JS keys off, accept-charset, etc.
labelledby string auto aria-labelledby target on the outer wrapper. Auto-derived as {$id}Label. Override only if the accessible name lives elsewhere in your custom header slot.

Slots:

Slot What it does
default The form body. Rows and hidden inputs go here.
header Replaces the auto-rendered header (close button + $title). Use for icons or dynamic titles that snipeit.js swaps at open time.
footer Replaces the auto-rendered footer (Cancel + Save buttons). Use only when you need a non-standard button set.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@snipe
snipe merged commit bd0831b into develop Sep 1, 2026
10 checks passed
@snipe
snipe deleted the modal-components-unification branch September 1, 2026 15:26
@snipe snipe changed the title Modal blade components unification Unified all modals into blade components Sep 1, 2026
@snipe snipe added the 🤜💥🤛 code-improvements Code structure improvements that should not change functionality label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤜💥🤛 code-improvements Code structure improvements that should not change functionality 📑 documentation update needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant