Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
231 changes: 227 additions & 4 deletions src/CrudPanelManager.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are failing on Github

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another round of testing. I discovered:

  • quite a few fields don't work select2s, browse
  • edit modal doesn't load the entry
  • after an entry is created with DataFormModal, sometimes the datatable refreshes, sometimes it doesn't

Here's a 5-min tour of my experience:
https://www.loom.com/share/6bffaa54f65e4c7f8b4188e064ca0bb7

Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,246 @@ 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();
if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) {

if ($crud->isInitialized() && $crud->getOperation() !== $operation && ! $shouldIsolate) {
self::setActiveController($controller::class);
$crud->initialized = false;

$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];
}

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

// If the panel isn't initialized at all, do full initialization
if (! $crud->isInitialized()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something here smells a bit - maybe we can return early or smth

// 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];

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

// If we reach here, the panel and operation are both initialized
// and the operation matches what was requested, so just return it
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
21 changes: 19 additions & 2 deletions src/app/Http/Controllers/Operations/CreateOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ protected function setupCreateDefaults()
LifecycleHook::hookInto('list:before_setup', function () {
$this->crud->addButton('top', 'create', 'view', 'crud::buttons.create');
});

LifecycleHook::hookInto('list:after_setup', function () {
// Check if modal form is enabled and replace the button if needed
$useModalForm = $this->crud->get('create.createButtonWithModalForm') ?? config('backpack.operations.create.createButtonWithModalForm', false);

if ($useModalForm) {
$this->crud->removeButton('create');
$this->crud->addButton('top', 'create', 'view', 'crud::buttons.create_in_modal', 'beginning');
}
});
}

/**
Expand All @@ -54,13 +64,20 @@ public function create()
{
$this->crud->hasAccessOrFail('create');

// Apply cached form setup if this is an AJAX request from a modal
if (request()->ajax() && request()->has('_modal_form_id')) {
\Backpack\CRUD\app\Library\Support\DataformCache::applySetupClosure($this->crud);
}

// prepare the fields you need to show
$this->data['crud'] = $this->crud;
$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 view($this->crud->getCreateView(), $this->data);
// return the ajax response for modal forms, or the normal view for normal requests
return request()->ajax() && request()->has('_modal_form_id') ?
view('crud::components.dataform.ajax_response', $this->data) :
view($this->crud->getCreateView(), $this->data);
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/app/Http/Controllers/Operations/UpdateOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ protected function setupUpdateDefaults()
LifecycleHook::hookInto(['list:before_setup', 'show:before_setup'], function () {
$this->crud->addButton('line', 'update', 'view', 'crud::buttons.update', 'end');
});

LifecycleHook::hookInto(['list:after_setup', 'show:after_setup'], function () {
// Check if modal form is enabled and replace the button if needed
$useModalForm = $this->crud->get('update.updateButtonWithModalForm') ?? config('backpack.operations.update.updateButtonWithModalForm', false);

if ($useModalForm) {
$this->crud->removeButton('update');
$this->crud->addButton('line', 'update', 'view', 'crud::buttons.update_in_modal', 'end');
}
});
}

/**
Expand All @@ -68,6 +78,11 @@ public function edit($id)
// get entry ID from Request (makes sure its the last ID for nested resources)
$id = $this->crud->getCurrentEntryId() ?? $id;

// Apply cached form setup if this is an AJAX request from a modal
if (request()->ajax() && request()->has('_modal_form_id')) {
\Backpack\CRUD\app\Library\Support\DataformCache::applySetupClosure($this->crud);
}

// register any Model Events defined on fields
$this->crud->registerFieldEvents();

Expand All @@ -82,7 +97,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() && request()->has('_modal_form_id') ?
view('crud::components.dataform.ajax_response', $this->data) :
view($this->crud->getEditView(), $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
Loading