Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7322594
wip
pxpm Sep 4, 2025
4a8fda0
Apply fixes from StyleCI
StyleCIBot Sep 4, 2025
642c120
wip
pxpm Sep 4, 2025
52b042a
wip
pxpm Sep 8, 2025
18ec4c8
Apply fixes from StyleCI
StyleCIBot Sep 8, 2025
9a6d8fe
wip
pxpm Sep 16, 2025
8dee927
Apply fixes from StyleCI
StyleCIBot Sep 16, 2025
3575976
remove specific settings initialization
pxpm Sep 16, 2025
68e4ac4
Merge branch 'modal-form' of https://github.com/Laravel-Backpack/CRUD…
pxpm Sep 16, 2025
cb89781
Apply fixes from StyleCI
StyleCIBot Sep 16, 2025
676f1c8
wip
pxpm Sep 16, 2025
945970f
fix modal response
pxpm Sep 19, 2025
c86a86e
wip
pxpm Oct 2, 2025
6e8f327
add modal form
pxpm Oct 10, 2025
bbe5763
Apply fixes from StyleCI
StyleCIBot Oct 10, 2025
79b31f7
wip
pxpm Oct 13, 2025
60cfbd5
Apply fixes from StyleCI
StyleCIBot Oct 13, 2025
5833621
wip
pxpm Oct 15, 2025
110c704
Merge branch 'modal-form' of https://github.com/Laravel-Backpack/CRUD…
pxpm Oct 15, 2025
f294e9a
Apply fixes from StyleCI
StyleCIBot Oct 15, 2025
524debf
fix DataformModal naming
tabacitu Oct 16, 2025
3dcef89
wip
pxpm Oct 16, 2025
0697c4c
Apply fixes from StyleCI
StyleCIBot Oct 16, 2025
1352a4c
wip
pxpm Oct 16, 2025
5938c35
Apply fixes from StyleCI
StyleCIBot Oct 16, 2025
f72c6d5
wip
pxpm Oct 17, 2025
6c63390
wip
pxpm Oct 20, 2025
b377f15
Apply fixes from StyleCI
StyleCIBot Oct 20, 2025
35ab10e
wip
pxpm Oct 20, 2025
4043d34
wip
pxpm Oct 20, 2025
50e63d5
wip
pxpm Oct 22, 2025
23d6395
wip
pxpm Oct 24, 2025
d24a2d8
Apply fixes from StyleCI
StyleCIBot Oct 24, 2025
d84b15a
wip
pxpm Oct 24, 2025
0baf87d
wip
pxpm Oct 24, 2025
2f3890a
Apply fixes from StyleCI
StyleCIBot Oct 24, 2025
dc68fa4
wip
pxpm Oct 24, 2025
70676d1
identation fixes
pxpm Oct 24, 2025
20ebccb
wip
pxpm Oct 24, 2025
6ba682c
default to non-modal create&edit
tabacitu Oct 27, 2025
f1c54fc
wip
pxpm Oct 28, 2025
6975675
Apply fixes from StyleCI
StyleCIBot Oct 28, 2025
4cfd01a
Update src/app/Http/Controllers/Operations/CreateOperation.php
tabacitu Oct 29, 2025
5245aae
wip
pxpm Oct 29, 2025
db8c2b3
Apply fixes from StyleCI
StyleCIBot Oct 29, 2025
c4305a9
wip
pxpm Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 196 additions & 4 deletions src/CrudPanelManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,36 @@ public function setupCrudPanel(string $controller, ?string $operation = null): C

// Use provided operation or default to 'list'
$operation = $operation ?? 'list';
$crud->setOperation($operation);

$shouldIsolate = $this->shouldIsolateOperation($controller::class, $operation);

$primaryControllerRequest = $this->cruds[array_key_first($this->cruds)]->getRequest();

// Check if we need to initialize this specific operation
if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) {
self::setActiveController($controller::class);
$crud->initialized = false;
self::setActiveController($controller::class);
$controller->initializeCrudPanel($primaryControllerRequest, $crud);

// If the panel isn't initialized at all, do full initialization
if (! $crud->isInitialized()) {
// Set the operation for full initialization
$crud->setOperation($operation);
$crud->initialized = false;
$controller->initializeCrudPanel($primaryControllerRequest, $crud);
} else {
// Panel is initialized, just setup this specific operation
// Use operation isolation for non-primary operations
if ($shouldIsolate) {
$this->setupIsolatedOperation($controller, $operation, $crud);
} else {
// Set the operation for standard setup
$crud->setOperation($operation);
$this->setupSpecificOperation($controller, $operation, $crud);
}
}

// Mark this operation as initialized
$this->storeInitializedOperation($controller::class, $operation);

self::unsetActiveController();
$crud = $this->cruds[$controller::class];

Expand All @@ -83,6 +105,176 @@ public function setupCrudPanel(string $controller, ?string $operation = null): C
return $this->cruds[$controller::class];
}

/**
* Determine if an operation should be isolated to prevent state interference.
*
* @param string $controller
* @param string $operation
* @return bool
*/
private function shouldIsolateOperation(string $controller, string $operation): bool
{
$currentCrud = $this->cruds[$controller] ?? null;
if (! $currentCrud) {
return false;
}

$currentOperation = $currentCrud->getOperation();

// Always isolate when switching between different operations
// This prevents any operation from interfering with another operation's state
if ($currentOperation && $currentOperation !== $operation) {
return true;
}

return false;
}

/**
* Setup an operation in isolation without affecting the main CRUD panel state.
* This creates a temporary context for operation setup without state interference.
*
* @param object $controller The controller instance
* @param string $operation The operation to setup
* @param CrudPanel $crud The CRUD panel instance
*/
private function setupIsolatedOperation($controller, string $operation, CrudPanel $crud): void
{
// Store the complete current state
$originalOperation = $crud->getOperation();
$originalSettings = $crud->settings();
$originalColumns = $crud->columns(); // Use the direct method, not operation setting
$originalRoute = $crud->route ?? null;
$originalEntityName = $crud->entity_name ?? null;
$originalEntityNamePlural = $crud->entity_name_plural ?? null;

// Store operation-specific settings generically
$originalOperationSettings = $this->extractOperationSettings($crud, $originalOperation);

// Temporarily setup the requested operation
$crud->setOperation($operation);

// Use the controller's own method to setup the operation properly
$reflection = new \ReflectionClass($controller);
$method = $reflection->getMethod('setupConfigurationForCurrentOperation');
$method->setAccessible(true);
$method->invoke($controller, $operation);

// Completely restore the original state
$crud->setOperation($originalOperation);

// CRITICAL: Properly restore columns by clearing and re-adding them
// This is essential to preserve list operation columns
$crud->removeAllColumns();
foreach ($originalColumns as $column) {
$crud->addColumn($column);
}

// Restore all original settings one by one, but skip complex objects
foreach ($originalSettings as $key => $value) {
try {
// Skip complex objects that Laravel generates dynamically
if (is_object($value) && (
$value instanceof \Illuminate\Routing\UrlGenerator ||
$value instanceof \Illuminate\Http\Request ||
$value instanceof \Illuminate\Contracts\Foundation\Application ||
$value instanceof \Closure ||
method_exists($value, '__toString') === false
)) {
continue;
}

$crud->set($key, $value);
} catch (\Exception $e) {
// Silently continue with restoration
}
}

// Restore operation-specific settings generically
$this->restoreOperationSettings($crud, $originalOperation, $originalOperationSettings);

// Restore core properties if they were changed
if ($originalRoute !== null) {
$crud->route = $originalRoute;
}
if ($originalEntityName !== null) {
$crud->entity_name = $originalEntityName;
}
if ($originalEntityNamePlural !== null) {
$crud->entity_name_plural = $originalEntityNamePlural;
}
}

/**
* Extract all settings for a specific operation.
*
* @param CrudPanel $crud The CRUD panel instance
* @param string $operation The operation name
* @return array Array of operation-specific settings
*/
private function extractOperationSettings(CrudPanel $crud, string $operation): array
{
$settings = $crud->settings();
$operationSettings = [];
$operationPrefix = $operation.'.';

foreach ($settings as $key => $value) {
if (str_starts_with($key, $operationPrefix)) {
$operationSettings[$key] = $value;
}
}

return $operationSettings;
}

/**
* Restore all settings for a specific operation.
*
* @param CrudPanel $crud The CRUD panel instance
* @param string $operation The operation name
* @param array $operationSettings The settings to restore
*/
private function restoreOperationSettings(CrudPanel $crud, string $operation, array $operationSettings): void
{
foreach ($operationSettings as $key => $value) {
try {
// Skip complex objects that Laravel generates dynamically
if (is_object($value) && (
$value instanceof \Illuminate\Routing\UrlGenerator ||
$value instanceof \Illuminate\Http\Request ||
$value instanceof \Illuminate\Contracts\Foundation\Application ||
$value instanceof \Closure ||
method_exists($value, '__toString') === false
)) {
continue;
}

$crud->set($key, $value);
} catch (\Exception $e) {
// Silently continue with restoration
}
}
}

/**
* Setup a specific operation without reinitializing the entire CRUD panel.
*
* @param object $controller The controller instance
* @param string $operation The operation to setup
* @param CrudPanel $crud The CRUD panel instance
*/
private function setupSpecificOperation($controller, string $operation, CrudPanel $crud): void
{
// Setup the specific operation using the existing CrudController infrastructure
$crud->setOperation($operation);

// Use the controller's own method to setup the operation properly
$reflection = new \ReflectionClass($controller);
$method = $reflection->getMethod('setupConfigurationForCurrentOperation');
$method->setAccessible(true);
$method->invoke($controller, $operation);
}

/**
* Check if a specific operation has been initialized for a controller.
*/
Expand Down
4 changes: 3 additions & 1 deletion src/app/Http/Controllers/Operations/CreateOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ public function create()
$this->data['title'] = $this->crud->getTitle() ?? trans('backpack::crud.add').' '.$this->crud->entity_name;

// load the view from /resources/views/vendor/backpack/crud/ if it exists, otherwise load the one in the package
return view($this->crud->getCreateView(), $this->data);
return request()->ajax() ?
view('crud::components.dataform.ajax_response', $this->data) :
view($this->crud->getCreateView(), $this->data);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/app/Http/Controllers/Operations/UpdateOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public function edit($id)
$this->data['id'] = $id;

// load the view from /resources/views/vendor/backpack/crud/ if it exists, otherwise load the one in the package
return view($this->crud->getEditView(), $this->data);
return request()->ajax() ?
view('crud::components.dataform.ajax_response', $this->data) :
view($this->crud->getEditView(), $this->data);
}

/**
Expand Down
18 changes: 10 additions & 8 deletions src/app/Library/CrudPanel/Traits/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Backpack\CRUD\ViewNamespaces;
use Carbon\Carbon;
use Validator;
use Illuminate\Support\Facades\Validator;

trait Search
{
Expand Down Expand Up @@ -275,14 +275,14 @@ public function getRowViews($entry, $rowNumber = false)
->render();
}

// add the bulk actions checkbox to the first column
if ($this->getOperationSetting('bulkActions')) {
// add the bulk actions checkbox to the first column - but only if we have columns
if ($this->getOperationSetting('bulkActions') && ! empty($row_items)) {
$bulk_actions_checkbox = \View::make('crud::columns.inc.bulk_actions_checkbox', ['entry' => $entry])->render();
$row_items[0] = $bulk_actions_checkbox.$row_items[0];
}

// add the details_row button to the first column
if ($this->getOperationSetting('detailsRow')) {
// add the details_row button to the first column - but only if we have columns
if ($this->getOperationSetting('detailsRow') && ! empty($row_items)) {
$details_row_button = \View::make('crud::columns.inc.details_row_button')
->with('crud', $this)
->with('entry', $entry)
Expand All @@ -291,7 +291,7 @@ public function getRowViews($entry, $rowNumber = false)
$row_items[0] = $details_row_button.$row_items[0];
}

if ($this->getResponsiveTable()) {
if ($this->getResponsiveTable() && ! empty($row_items)) {
$responsiveTableTrigger = '<div class="dtr-control d-none cursor-pointer"></div>';
$row_items[0] = $responsiveTableTrigger.$row_items[0];
}
Expand Down Expand Up @@ -398,16 +398,18 @@ public function getEntriesAsJsonForDatatables($entries, $totalRows, $filteredRow
{
$rows = [];

foreach ($entries as $row) {
foreach ($entries as $index => $row) {
$rows[] = $this->getRowViews($row, $startIndex === false ? false : ++$startIndex);
}

return [
$result = [
'draw' => (isset($this->getRequest()['draw']) ? (int) $this->getRequest()['draw'] : 0),
'recordsTotal' => $totalRows,
'recordsFiltered' => $filteredRows,
'data' => $rows,
];

return $result;
}

/**
Expand Down
65 changes: 65 additions & 0 deletions src/app/View/Components/DataFormModal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Backpack\CRUD\app\View\Components;

use Closure;

class DataformModal extends DataForm
{
/**
* Create a new component instance.
*
* @param string $controller The CRUD controller class name
* @param string $operation The operation to use (create, update, etc.)
* @param string|null $action Custom form action URL
* @param string $method Form method (post, put, etc.)
* @param bool $hasUploadFields Whether the form has upload fields
* @param mixed|null $entry The model instance for update operations
* @param Closure|null $setup A closure to customize the CRUD panel
* @param string $formRouteOperation The operation to use for the form route (defaults to 'create')
* @param string $id The ID for the form element (defaults to 'backpack-form')
* @param bool $focusOnFirstField Whether to focus on the first field when form loads
* @param string $title The title of the modal
* @param string $classes CSS classes for the modal dialog
* @param bool $refreshDatatable Whether to refresh the datatable after form submission
*/
public function __construct(
public string $controller,
public string $id = 'backpack-form',
public string $operation = 'create',
public string $name = '',
public string $formRouteOperation = 'create',
public ?string $action = null,
public string $method = 'post',
public bool $hasUploadFields = false,
public $entry = null,
public ?Closure $setup = null,
public bool $focusOnFirstField = false,
public string $title = 'Form',
public string $classes = 'modal-dialog modal-lg',
public bool $refreshDatatable = false,
) {
parent::__construct($controller, $id, $name, $operation, $action, $method, $hasUploadFields, $entry, $setup, $focusOnFirstField);
}

/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('crud::components.dataform.modal-form', [
'crud' => $this->crud,
'id' => $this->id,
'operation' => $this->operation,
'formRouteOperation' => $this->formRouteOperation,
'hasUploadFields' => $this->hasUploadFields,
'refreshDatatable' => $this->refreshDatatable,
'action' => $this->action,
'method' => $this->method,
'title' => $this->title,
'classes' => $this->classes,
]);
}
}
1 change: 0 additions & 1 deletion src/app/View/Components/Dataform.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public function __construct(
public $entry = null,
public ?Closure $setup = null,
public bool $focusOnFirstField = false,

) {
// Get CRUD panel instance from the controller
CrudManager::setActiveController($controller);
Expand Down
5 changes: 4 additions & 1 deletion src/resources/assets/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
--table-row-hover: #f2f1ff;
--select2-selected-item-background: #7c69ef;
--select2-selected-item-color: #fff;
--btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
}

.sidebar .nav-dropdown-items .nav-dropdown {
Expand Down Expand Up @@ -254,7 +255,9 @@ div[id$="_wrapper"] .dt-processing {
.modal .details-control {
display: none;
}

.modal .btn-close, .modal .close {
background: transparent var(--btn-close-bg) center / .75rem auto no-repeat;
}
.dtr-bs-modal .modal-body {
padding: 0;
}
Expand Down
Loading
Loading