Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 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
257 changes: 249 additions & 8 deletions src/CrudPanelManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,272 @@ public function getCrudPanel(CrudControllerContract|string $controller): CrudPan
*/
public function setupCrudPanel(string $controller, ?string $operation = null): CrudPanel
{
// Resolve potential active controller and ensure we have an instance
$controller = $this->getActiveController() ?? $controller;

$controller = is_string($controller) ? app($controller) : $controller;

$crud = $this->getCrudPanel($controller);

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

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

// primary controller request is used when doing a full initialization
$primaryControllerRequest = $this->cruds[array_key_first($this->cruds)]->getRequest();

// If the panel is already initialized but a different operation is requested
// and we don't need to isolate that operation, do a simple setup and return early.
if ($crud->isInitialized() && $crud->getOperation() !== $operation && ! $shouldIsolate) {
return $this->performSimpleOperationSwitch($controller, $operation, $crud);
}

// If the panel (or this specific operation) hasn't been initialized yet,
// perform the required initialization (full or operation-specific).
if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) {
self::setActiveController($controller::class);
return $this->performInitialization($controller, $operation, $crud, $primaryControllerRequest, $shouldIsolate);
}

// Already initialized and operation matches: nothing to do.
return $this->cruds[$controller::class];
}

/**
* Perform a lightweight operation switch when the panel is initialized and
* isolation is not required.
*/
private function performSimpleOperationSwitch($controller, string $operation, CrudPanel $crud): CrudPanel
{
self::setActiveController($controller::class);

$crud->setOperation($operation);
$this->setupSpecificOperation($controller, $operation, $crud);

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

self::unsetActiveController();

return $this->cruds[$controller::class];
}

/**
* Perform full or operation-specific initialization when needed.
*/
private function performInitialization($controller, string $operation, CrudPanel $crud, $primaryControllerRequest, bool $shouldIsolate): CrudPanel
{
self::setActiveController($controller::class);

// 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;
self::setActiveController($controller::class);
$controller->initializeCrudPanel($primaryControllerRequest, $crud);
self::unsetActiveController();
$crud = $this->cruds[$controller::class];

return $this->cruds[$controller::class];
} else {
// Panel is initialized, just setup this specific operation
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();

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();

// If operations don't differ, no need to isolate
if (! $currentOperation || $currentOperation === $operation) {
return false;
}

// Check backtrace for components implementing IsolatesOperationSetup
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 10);

foreach ($backtrace as $trace) {
if (isset($trace['object'])) {
$object = $trace['object'];

// If we find a component that implements the interface, use its declared behavior
if ($object instanceof \Backpack\CRUD\app\View\Components\Contracts\IsolatesOperationSetup) {
return $object->shouldIsolateOperationSetup();
}
}
}

return true;
}

/**
* 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);

$controller->setup();

// 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
3 changes: 2 additions & 1 deletion src/app/Http/Controllers/Operations/CreateOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protected function setupCreateDefaults()
$this->crud->allowAccess('create');

LifecycleHook::hookInto('create:before_setup', function () {
$this->crud->loadDefaultOperationSettingsFromConfig();
$this->crud->setupDefaultSaveActions();
});

Expand All @@ -59,7 +60,7 @@ public function create()
$this->data['saveAction'] = $this->crud->getSaveAction();
$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 the ajax response for modal forms, or the normal view for normal requests
return view($this->crud->getCreateView(), $this->data);
}

Expand Down
10 changes: 8 additions & 2 deletions src/app/Library/CrudPanel/Traits/Read.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,14 @@ public function getDefaultPageLength()
*/
public function addCustomPageLengthToPageLengthMenu()
{
$values = $this->getOperationSetting('pageLengthMenu')[0];
$labels = $this->getOperationSetting('pageLengthMenu')[1];
$pageLengthMenu = $this->getOperationSetting('pageLengthMenu');

if (is_null($pageLengthMenu)) {
return;
}

$values = $pageLengthMenu[0];
$labels = $pageLengthMenu[1];

if (array_search($this->getDefaultPageLength(), $values) === false) {
for ($i = 0; $i < count($values); $i++) {
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
Loading