diff --git a/src/CrudPanelManager.php b/src/CrudPanelManager.php index 86ca718d10..d068069c08 100644 --- a/src/CrudPanelManager.php +++ b/src/CrudPanelManager.php @@ -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()) { + // 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. */ diff --git a/src/app/Http/Controllers/Operations/CreateOperation.php b/src/app/Http/Controllers/Operations/CreateOperation.php index e6282ff68f..d0aeb47b8c 100644 --- a/src/app/Http/Controllers/Operations/CreateOperation.php +++ b/src/app/Http/Controllers/Operations/CreateOperation.php @@ -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'); + } + }); } /** @@ -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); } /** diff --git a/src/app/Http/Controllers/Operations/UpdateOperation.php b/src/app/Http/Controllers/Operations/UpdateOperation.php index f0c656092c..9d73836449 100644 --- a/src/app/Http/Controllers/Operations/UpdateOperation.php +++ b/src/app/Http/Controllers/Operations/UpdateOperation.php @@ -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'); + } + }); } /** @@ -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(); @@ -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); } /** diff --git a/src/app/Library/CrudPanel/Traits/Read.php b/src/app/Library/CrudPanel/Traits/Read.php index fa30de949d..4175e9f8ff 100644 --- a/src/app/Library/CrudPanel/Traits/Read.php +++ b/src/app/Library/CrudPanel/Traits/Read.php @@ -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++) { diff --git a/src/app/Library/CrudPanel/Traits/Search.php b/src/app/Library/CrudPanel/Traits/Search.php index c3c253f384..bbfe4a1d6e 100644 --- a/src/app/Library/CrudPanel/Traits/Search.php +++ b/src/app/Library/CrudPanel/Traits/Search.php @@ -4,7 +4,7 @@ use Backpack\CRUD\ViewNamespaces; use Carbon\Carbon; -use Validator; +use Illuminate\Support\Facades\Validator; trait Search { @@ -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) @@ -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 = '
'; $row_items[0] = $responsiveTableTrigger.$row_items[0]; } @@ -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; } /** diff --git a/src/app/Library/Support/DataformCache.php b/src/app/Library/Support/DataformCache.php new file mode 100644 index 0000000000..6661bc1d3c --- /dev/null +++ b/src/app/Library/Support/DataformCache.php @@ -0,0 +1,218 @@ +cachePrefix = 'dataform_config_'; + $this->cacheDuration = 60; // 1 hour + } + + /** + * Cache setup closure for a dataform component. + * + * @param string $formId The form ID to use as cache key + * @param string $controllerClass The controller class + * @param array $fieldsConfig The field configuration after setup was applied + * @param string|null $name The element name + * @param CrudPanel $crud The CRUD panel instance to update with form_id + * @return bool Whether the operation was successful + */ + public function cacheForComponent(string $formId, string $controllerClass, array $fieldsConfig, ?string $name = null, ?CrudPanel $crud = null): bool + { + if (empty($fieldsConfig)) { + return false; + } + + $cruds = CrudManager::getCrudPanels(); + $parentCrud = reset($cruds); + + $parentEntry = null; + $parentController = null; + + if ($parentCrud && $parentCrud->getCurrentEntry()) { + $parentEntry = $parentCrud->getCurrentEntry(); + $parentController = $parentCrud->controller; + } + + // Store metadata in cache (even without parent entry for standalone modal forms) + $this->store( + $formId, + $controllerClass, + $parentController, + $parentEntry, + $name + ); + + // Store the field configuration in Laravel cache (persists across requests) + \Cache::put($this->cachePrefix.$formId.'_fields', $fieldsConfig, now()->addMinutes($this->cacheDuration)); + + // Set the form_id in the CRUD panel if provided + if ($crud) { + $crud->set('form.form_id', $formId); + } + + return true; + } + + public static function applyAndStoreSetupClosure( + string $formId, + string $controllerClass, + \Closure $setupClosure, + ?string $name = null, + ?CrudPanel $crud = null, + $parentEntry = null + ): bool { + $instance = new self(); + // Apply the setup closure to the CrudPanel instance + if ($instance->applyClosure($crud, $controllerClass, $setupClosure, $parentEntry)) { + // Capture the resulting field configuration after setup + $fieldsAfterSetup = []; + foreach ($crud->fields() as $fieldName => $field) { + $fieldsAfterSetup[$fieldName] = $field; + } + + // Cache the field configuration (not the closure, since it won't persist across requests) + $cached = $instance->cacheForComponent($formId, $controllerClass, $fieldsAfterSetup, $name, $crud); + + return $cached; + } + + return false; + } + + /** + * Apply cached setup to a CRUD instance using the request's form_id. + * + * @param CrudPanel $crud The CRUD panel instance + * @return bool Whether the operation was successful + */ + public static function applySetupClosure(CrudPanel $crud): bool + { + $instance = new self(); + // Check if the request has a _modal_form_id parameter + $formId = request('_modal_form_id'); + + if (! $formId) { + return false; + } + + return $instance->apply($formId, $crud); + } + + /** + * Apply a setup closure to a CrudPanel instance. + * + * @param CrudPanel $crud The CRUD panel instance + * @param string $controllerClass The controller class + * @param \Closure $setupClosure The setup closure + * @param mixed $entry The entry to pass to the setup closure + * @return bool Whether the operation was successful + */ + private function applyClosure(CrudPanel $crud, string $controllerClass, \Closure $setupClosure, $entry = null): bool + { + $originalSetup = $setupClosure; + $modifiedSetup = function ($crud, $entry) use ($originalSetup, $controllerClass) { + CrudManager::setActiveController($controllerClass); + + // Run the original closure + return ($originalSetup)($crud, $entry); + }; + + try { + // Execute the modified closure + ($modifiedSetup)($crud, $entry); + + return true; + } catch (\Exception $e) { + return false; + } finally { + // Clean up + CrudManager::unsetActiveController(); + } + } + + /** + * Prepare dataform data for storage in the cache. + * + * @param string $controllerClass The controller class + * @param string $parentController The parent controller + * @param mixed $parentEntry The parent entry + * @param string|null $elementName The element name + * @return array The data to be cached + */ + protected function prepareDataForStorage(...$args): array + { + [$controllerClass, $parentController, $parentEntry, $elementName] = $args; + + return [ + 'controller' => $controllerClass, + 'parentController' => $parentController, + 'parent_entry' => $parentEntry, + 'element_name' => $elementName, + 'operations' => $parentController ? CrudManager::getInitializedOperations($parentController) : [], + ]; + } + + /** + * Apply data from the cache to configure a dataform. + * + * @param array $cachedData The cached data + * @param CrudPanel $crud The CRUD panel instance + * @return bool Whether the operation was successful + */ + protected function applyFromCache($cachedData, ...$args): bool + { + [$crud] = $args; + + try { + // Initialize operations for the parent controller (if it exists) + if (! empty($cachedData['parentController'])) { + $this->initializeOperations($cachedData['parentController'], $cachedData['operations']); + } + $entry = $cachedData['parent_entry'] ?? null; + + // Get the form_id from the cached data + $formId = $crud->get('form.form_id') ?? request()->input('_form_id'); + + // Retrieve the field configuration from Laravel cache + if ($formId) { + $fieldsConfig = \Cache::get($this->cachePrefix.$formId.'_fields'); + + if ($fieldsConfig && is_array($fieldsConfig)) { + // Clear all existing fields + $crud->setOperationSetting('fields', []); + + // Restore the cached field configuration + foreach ($fieldsConfig as $fieldName => $field) { + $crud->addField($field); + } + + return true; + } + } + + return false; + } catch (\Exception $e) { + return false; + } + } + + /** + * Initialize operations for a parent controller. + */ + private function initializeOperations(string $parentController, $operations): void + { + $parentCrud = CrudManager::setupCrudPanel($parentController); + + foreach ($operations as $operation) { + $parentCrud->initialized = false; + CrudManager::setupCrudPanel($parentController, $operation); + } + } +} diff --git a/src/app/View/Components/Contracts/IsolatesOperationSetup.php b/src/app/View/Components/Contracts/IsolatesOperationSetup.php new file mode 100644 index 0000000000..7f905616bd --- /dev/null +++ b/src/app/View/Components/Contracts/IsolatesOperationSetup.php @@ -0,0 +1,29 @@ +crud = CrudManager::setupCrudPanel($controller, $operation); + $this->crud = CrudManager::setupCrudPanel($controller, $this->formOperation); + + if ($this->crud->getOperation() !== $this->formOperation) { + $this->crud->setOperation($this->formOperation); + } $this->crud->setAutoFocusOnFirstField($this->focusOnFirstField); - if ($this->entry && $this->operation === 'update') { - $this->action = $action ?? url($this->crud->route.'/'.$this->entry->getKey()); - $this->method = 'put'; + if ($this->entry && $this->formOperation === 'update') { + $this->formAction = $formAction ?? url($this->crud->route.'/'.$this->entry->getKey()); + $this->formMethod = 'put'; $this->crud->entry = $this->entry; $this->crud->setOperationSetting('fields', $this->crud->getUpdateFields()); } else { - $this->action = $action ?? url($this->crud->route); + $this->formAction = $formAction ?? url($this->crud->route); } - $this->hasUploadFields = $this->crud->hasUploadFields($operation, $this->entry?->getKey()); - $this->id = $id.md5($this->action.$this->operation.$this->method.$this->controller); + + $this->hasUploadFields = $this->crud->hasUploadFields($this->formOperation, $this->entry?->getKey()); + $this->id = $id.md5($this->formAction.$this->formOperation.$this->formMethod.$this->controller); if ($this->setup) { - $this->applySetupClosure(); + $parentEntry = $this->getParentCrudEntry(); + call_user_func($this->setup, $this->crud, $parentEntry); } + // Reset the active controller CrudManager::unsetActiveController(); } - public function applySetupClosure(): bool + private function getParentCrudEntry() { - $originalSetup = $this->setup; - $controllerClass = $this->controller; - $crud = $this->crud; - $entry = $this->entry; - - $modifiedSetup = function ($crud, $entry) use ($originalSetup, $controllerClass) { - CrudManager::setActiveController($controllerClass); - - // Run the original closure - return ($originalSetup)($crud, $entry); - }; - - try { - // Execute the modified closure - ($modifiedSetup)($crud, $entry); - - return true; - } finally { - // Clean up - CrudManager::unsetActiveController(); + $cruds = CrudManager::getCrudPanels(); + $parentCrud = reset($cruds); + + if ($parentCrud && $parentCrud->getCurrentEntry()) { + CrudManager::storeInitializedOperation( + $parentCrud->controller, + $parentCrud->getCurrentOperation() + ); + + return $parentCrud->getCurrentEntry(); } + + return null; } /** @@ -90,16 +98,16 @@ public function applySetupClosure(): bool public function render() { // Store the current form ID in the service container for form-aware old() helper - app()->instance('backpack.current_form_id', $this->id); + app()->instance('backpack.current_form_modal_id', $this->id); return view('crud::components.dataform.form', [ 'crud' => $this->crud, 'saveAction' => $this->crud->getSaveAction(), 'id' => $this->id, 'name' => $this->name, - 'operation' => $this->operation, - 'action' => $this->action, - 'method' => $this->method, + 'formOperation' => $this->formOperation, + 'formAction' => $this->formAction, + 'formMethod' => $this->formMethod, 'hasUploadFields' => $this->hasUploadFields, 'entry' => $this->entry, ]); diff --git a/src/app/View/Components/DataformModal.php b/src/app/View/Components/DataformModal.php new file mode 100644 index 0000000000..5b33f0c96f --- /dev/null +++ b/src/app/View/Components/DataformModal.php @@ -0,0 +1,112 @@ +route === null) { + \Backpack\CRUD\CrudManager::setActiveController($controller); + $tempCrud = \Backpack\CRUD\CrudManager::setupCrudPanel($controller, $this->formOperation); + $this->route = $tempCrud->route; + \Backpack\CRUD\CrudManager::unsetActiveController(); + } + + // keep backwards compatible behavior for resolving route when not provided + $this->formAction = $this->formAction ?? url($this->route); + + // Generate the SAME hashed form ID that the Dataform component uses + $this->hashedFormId = $this->id.md5($this->formAction.$this->formOperation.'post'.$this->controller); + + // Cache the setup closure if provided (for retrieval during AJAX request) + if ($this->setup instanceof \Closure) { + $this->cacheSetupClosure(); + } + + // DO NOT call parent::__construct() because we don't want to initialize + // the CRUD panel on page load - the form will be loaded via AJAX + + if ($this->entry && $this->formOperation === 'update') { + // Use the resolved action (base route) to build the edit URL for the entry + $this->formUrl = url($this->formAction.'/'.$this->entry->getKey().'/edit'); + } + } + + /** + * Cache the setup closure for later retrieval during AJAX form load. + */ + protected function cacheSetupClosure(): void + { + // Create a temporary CRUD instance to apply and cache the setup + \Backpack\CRUD\CrudManager::setActiveController($this->controller); + $tempCrud = \Backpack\CRUD\CrudManager::setupCrudPanel($this->controller, $this->formOperation); + + // Apply and cache the setup closure using the HASHED ID + \Backpack\CRUD\app\Library\Support\DataformCache::applyAndStoreSetupClosure( + $this->hashedFormId, // Use the hashed ID that matches what Dataform component generates + $this->controller, + $this->setup, + null, + $tempCrud, + null + ); + + \Backpack\CRUD\CrudManager::unsetActiveController(); + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + // We don't need $crud here because the modal loads the form via AJAX + // The CRUD panel will be initialized when the AJAX request is made + return view('crud::components.dataform.modal-form', [ + 'id' => $this->id, + 'formOperation' => $this->formOperation, + 'formUrl' => $this->formUrl, + 'hasUploadFields' => $this->hasUploadFields, + 'refreshDatatable' => $this->refreshDatatable, + 'formAction' => $this->formAction, + 'formMethod' => $this->formMethod, + 'title' => $this->title, + 'classes' => $this->classes, + 'hashedFormId' => $this->hashedFormId, + 'controller' => $this->controller, + 'route' => $this->route, // Pass the route for building URLs in the template + ]); + } +} diff --git a/src/app/View/Components/Datatable.php b/src/app/View/Components/Datatable.php index b906c1be04..66664be734 100644 --- a/src/app/View/Components/Datatable.php +++ b/src/app/View/Components/Datatable.php @@ -4,13 +4,23 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanel; use Backpack\CRUD\app\Library\Support\DatatableCache; +use Backpack\CRUD\app\View\Components\Contracts\IsolatesOperationSetup; use Backpack\CRUD\CrudManager; use Illuminate\View\Component; -class Datatable extends Component +class Datatable extends Component implements IsolatesOperationSetup { protected string $tableId; + /** + * Datatables do NOT isolate their operation setup. + * They manage their own operation state independently. + */ + public function shouldIsolateOperationSetup(): bool + { + return false; + } + public function __construct( private string $controller, private ?CrudPanel $crud = null, @@ -23,10 +33,13 @@ public function __construct( $this->crud ??= CrudManager::setupCrudPanel($controller, 'list'); + if ($this->crud->getOperation() !== 'list') { + $this->crud->setOperation('list'); + } + $this->tableId = $this->generateTableId(); - if ($this->setup) { - // Apply the configuration using DatatableCache + if ($this->setup) { // Apply the configuration using DatatableCache DatatableCache::applyAndStoreSetupClosure( $this->tableId, $this->controller, @@ -38,7 +51,12 @@ public function __construct( } if (! $this->crud->has('list.datatablesUrl')) { - $this->crud->set('list.datatablesUrl', $this->crud->getRoute()); + $route = $this->crud->getRoute(); + // If route is not set, generate it from the controller + if (empty($route)) { + $route = action([$this->controller, 'index']); + } + $this->crud->set('list.datatablesUrl', $route); } // Reset the active controller @@ -77,6 +95,7 @@ public function render() 'crud' => $this->crud, 'modifiesUrl' => $this->modifiesUrl, 'tableId' => $this->tableId, + 'datatablesUrl' => url($this->crud->get('list.datatablesUrl')), ]); } } diff --git a/src/config/backpack/operations/create.php b/src/config/backpack/operations/create.php index 2bdef21f18..66c4f267a4 100644 --- a/src/config/backpack/operations/create.php +++ b/src/config/backpack/operations/create.php @@ -21,6 +21,9 @@ // when the page loads, put the cursor on the first input? 'autoFocusOnFirstField' => true, + // when enabled, the create button will open a modal form instead of the create page + 'createButtonWithModalForm' => false, + // Where do you want to redirect the user by default, save? // options: save_and_back, save_and_edit, save_and_new 'defaultSaveAction' => 'save_and_back', diff --git a/src/config/backpack/operations/update.php b/src/config/backpack/operations/update.php index 27fe127475..784bb17c6e 100644 --- a/src/config/backpack/operations/update.php +++ b/src/config/backpack/operations/update.php @@ -21,6 +21,9 @@ // when the page loads, put the cursor on the first input? 'autoFocusOnFirstField' => true, + // when enabled, the update button will open a modal form instead of the edit page + 'updateButtonWithModalForm' => false, + // Where do you want to redirect the user by default, save? // options: save_and_back, save_and_edit, save_and_new 'defaultSaveAction' => 'save_and_back', diff --git a/src/resources/assets/css/common.css b/src/resources/assets/css/common.css index 4c6a13ac23..77d8d7c79d 100644 --- a/src/resources/assets/css/common.css +++ b/src/resources/assets/css/common.css @@ -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 { @@ -123,6 +124,13 @@ div[id$="_wrapper"] .dt-processing { z-index: 999999 !important; } +/* Ensure Select2 dropdowns are visible and properly positioned in all contexts */ +/* Bootstrap modals use z-index 1050-1055, so Select2 needs to be higher */ +.select2-in-modal-repeatable .select2-dropdown { + z-index: 1060 !important; + position: fixed !important; +} + .navbar-filters { min-height: 25px; border-radius: 0; @@ -254,7 +262,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; } diff --git a/src/resources/views/crud/buttons/create.blade.php b/src/resources/views/crud/buttons/create.blade.php index 483f62d703..1056708fdf 100644 --- a/src/resources/views/crud/buttons/create.blade.php +++ b/src/resources/views/crud/buttons/create.blade.php @@ -1,4 +1,5 @@ @if ($crud->hasAccess('create')) + {{-- Regular create button that redirects to create page --}} {{ trans('backpack::crud.add') }} {{ $crud->entity_name }} diff --git a/src/resources/views/crud/buttons/create_in_modal.blade.php b/src/resources/views/crud/buttons/create_in_modal.blade.php new file mode 100644 index 0000000000..620a9f2a47 --- /dev/null +++ b/src/resources/views/crud/buttons/create_in_modal.blade.php @@ -0,0 +1,27 @@ +@if ($crud->hasAccess('create')) + @php + $controllerClass = get_class(app('request')->route()->getController()); + @endphp + + {{-- Create button that opens modal form --}} + + + {{-- Include the modal form component --}} +