Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
200 changes: 196 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,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()) {
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];

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