diff --git a/config/schema/panelizer.schema.yml b/config/schema/panelizer.schema.yml index 9b91bd3..8b873a9 100644 --- a/config/schema/panelizer.schema.yml +++ b/config/schema/panelizer.schema.yml @@ -1,4 +1,14 @@ # Config schema for Panelizer. +panelizer.default.display: + type: display_variant.plugin.panels_variant + label: 'Panelizer display default' + mapping: + static_context: + type: ctools.context + label: 'Contexts' + pattern: + type: string + label: 'Pattern plugin ID' core.entity_view_display.*.*.*.third_party.panelizer: type: mapping @@ -9,10 +19,15 @@ core.entity_view_display.*.*.*.third_party.panelizer: custom: type: boolean description: 'Are custom overrides allowed?' + allow: + type: boolean + description: 'Is choosing between available defaults during entity creation allowed?' + default: + type: string + description: 'Default display ID' displays: type: sequence description: 'Default displays' sequence: - type: display_variant.plugin.panels_variant - description: 'Default display' - + type: panelizer.default.display + description: 'Default displays' diff --git a/css/panelizer.admin.css b/css/panelizer.admin.css new file mode 100644 index 0000000..1aa3794 --- /dev/null +++ b/css/panelizer.admin.css @@ -0,0 +1,113 @@ +/** + * @file + * Styles for Panelizer wizard admin. + */ + +/* Narrow screens */ + +.panelizer-wizard-tree, +.panelizer-wizard-form { + box-sizing: border-box; +} + +/** + * Wizard actions across the top. + */ +.panelizer-wizard-actions { + text-align: right; /* LTR */ +} +.panelizer-wizard-actions ul.inline, +.panelizer-wizard-actions ul.inline li { + display: inline-block; + margin: 0; +} +.panelizer-wizard-actions ul.inline { + border-top: 1px solid black; + border-left: 1px solid black; +} +.panelizer-wizard-actions ul.inline li { + border-right: 1px solid black; + padding: .5em; +} + +/** + * The tree of wizard steps. + */ +.panelizer-wizard-tree ul { + margin: 0; + padding: 0; + list-style: none; +} +.panelizer-wizard-tree ul > li > ul { + margin-left: 1em; +} +.panelizer-wizard-tree > ul { + border: 1px solid black; + padding-bottom: .5em; + margin-bottom: 20px; +} +.panelizer-wizard-tree li { + border-bottom: 1px solid black; + padding: .5em; + padding-right: 0; +} +.panelizer-wizard-tree li:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +/** + * The wizard form. + */ +.panelizer-wizard-form { + border: 1px solid black; + padding: 1em; + margin-bottom: 20px; +} + +/* Wide screens */ +@media + screen and (min-width: 780px), + (orientation: landscape) and (min-device-height: 780px) { + + /** + * Overall layout. + */ + .panelizer-wizard-tree { + float: left; /* LTR */ + width: 20%; + } + .panelizer-wizard-form { + float: left; /* LTR */ + width: 80%; + } + .panelizer-wizard-form-actions { + margin-left: 20%; /* LTR */ + } + + /** + * Make the borders look nice. + */ + .panelizer-wizard-tree > ul { + border-right: 0; /* LTR */ + } + .panelizer-wizard-form { + min-height: 400px; + } + + /** + * Right-to-left support. + */ + [dir="rtl"] .panelizer-wizard-tree, + [dir="rtl"] .panelizer-wizard-form { + float: right; + } + [dir="rtl"] .panelizer-wizard-form-actions { + margin-left: 0; + margin-right: 20%; + } + [dir="rtl"] .panelizer-wizard-tree > ul { + border-right: 1px solid black; + border-left: 0; + } +} diff --git a/js/panels_ipe/panels_ipe.js b/js/panels_ipe/panels_ipe.js index 5101a64..215a754 100644 --- a/js/panels_ipe/panels_ipe.js +++ b/js/panels_ipe/panels_ipe.js @@ -32,10 +32,7 @@ revert_tab.set({loading: true}); $.ajax({ url: drupalSettings.path.baseUrl + 'admin/panelizer/panels_ipe/' + entity.entity_type_id + '/' + entity.entity_id + '/' + entity.view_mode + '/revert_to_default', - data: { - // @todo: Use the default this entity is on. - default: 'default' - }, + data: {}, type: 'POST' }).done(function (data) { location.reload(); diff --git a/panelizer.libraries.yml b/panelizer.libraries.yml index 6e9a5b2..50b9c76 100644 --- a/panelizer.libraries.yml +++ b/panelizer.libraries.yml @@ -17,4 +17,8 @@ panels_ipe: css/panels_ipe.css: {} dependencies: - panels_ipe/panels_ipe - +wizard_admin: + version: VERSION + css: + layout: + css/panelizer.admin.css: {} diff --git a/panelizer.links.action.yml b/panelizer.links.action.yml new file mode 100644 index 0000000..14e71da --- /dev/null +++ b/panelizer.links.action.yml @@ -0,0 +1,5 @@ +panelizer.default.add: + route_name: panelizer.wizard.add + title: 'Add panelizer default' + class: \Drupal\panelizer\Menu\AddDefaultLocalAction + deriver: \Drupal\panelizer\Plugin\AddDefaultLinkDeriver diff --git a/panelizer.module b/panelizer.module index 235bd45..0bb7ff5 100644 --- a/panelizer.module +++ b/panelizer.module @@ -4,10 +4,11 @@ * Hook implementations for the Panelizer module. */ -use \Drupal\Core\Entity\FieldableEntityInterface; -use \Drupal\Core\Entity\RevisionableInterface; -use \Drupal\Core\Form\FormStateInterface; -use \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\RevisionableInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant; /** * Implements hook_theme(). @@ -17,10 +18,62 @@ function panelizer_theme() { 'panelizer_view_mode' => [ 'render element' => 'element', ], + 'panelizer_wizard_form' => [ + 'render element' => 'form', + ], + 'panelizer_wizard_tree' => [ + 'variables' => [ + 'wizard' => NULL, + 'cached_values' => [], + 'tree' => [], + 'divider' => ' ยป ', + 'step' => NULL, + ], + ], ]; } /** + * Preprocess function for panelizer-wizard-tree.html.twig. + */ +function template_preprocess_panelizer_wizard_tree(&$variables) { + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */ + $wizard = $variables['wizard']; + $cached_values = $variables['cached_values']; + $tree = $variables['tree']; + $variables['step'] = $wizard->getStep($cached_values); + + foreach ($wizard->getOperations($cached_values) as $step => $operation) { + $parameters = $wizard->getNextParameters($cached_values); + // Override step to be the step we want. + $parameters['step'] = $step; + + // Fill in parents if there are breadcrumbs. + $parent =& $tree; + if (isset($operation['breadcrumbs'])) { + foreach ($operation['breadcrumbs'] as $breadcrumb) { + $breadcrumb_string = (string) $breadcrumb; + if (!isset($parent[$breadcrumb_string])) { + $parent[$breadcrumb_string] = [ + 'title' => $breadcrumb, + 'children' => [], + ]; + } + $parent =& $parent[$breadcrumb_string]['children']; + } + } + + $parent[$step] = [ + 'title' => !empty($operation['title']) ? $operation['title'] : '', + 'url' => new Url($wizard->getRouteName(), $parameters), + 'step' => $step, + ]; + } + + $variables['tree'] = $tree; +} + +/** * Implements hook_entity_type_alter(). */ function panelizer_entity_type_alter(array &$entity_types) { @@ -60,7 +113,7 @@ function panelizer_panels_build_alter(&$build, PanelsDisplayVariant $panels_disp list (,, $view_mode) = explode(':', $panels_display->getStorageId()); // Get the default storage id. - $default = 'default'; + list(,,, $default) = explode(':', $panels_display->getStorageId()); if ($panels_display->getStorageType() == 'panelizer_field') { $panelizer_default_storage_id = implode(':', [$entity->getEntityTypeId(), $entity->bundle(), $view_mode, $default]); } @@ -148,7 +201,7 @@ function panelizer_panels_ipe_panels_display_presave(PanelsDisplayVariant $panel } /** - * Implements hook_form_alter(). + * Implements hook_form_FORM_ID_alter(). */ function panelizer_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) { /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ @@ -167,45 +220,109 @@ function panelizer_form_entity_view_display_edit_form_alter(&$form, FormStateInt $form['panelizer'] = [ '#tree' => TRUE, ]; + // If this display mode is panelized, then show a link to its settings. + if (!empty($settings['enable'])) { + $form['#cache']['tags'][] = "panelizer_default:{$display->getTargetEntityTypeId()}:{$display->getTargetBundle()}:{$display->getMode()}"; + $form['panelizer']['displays'] = [ + '#type' => 'table', + '#caption' => t('Panelized Displays'), + '#header' => [t('Label'), t('Default'), t('Operations')], + ]; + foreach ($display->getThirdPartySetting('panelizer', 'displays', []) as $machine_name => $panels_display) { + // Reset operations when in the foreach loop. + $operations = []; + $display_name = $machine_name; + $machine_name ="{$display->getTargetEntityTypeId()}__{$display->getTargetBundle()}__{$display->getMode()}__$machine_name"; + $operations['edit'] = [ + 'title' => t('Edit'), + 'url' => Url::fromRoute('panelizer.wizard.edit', ['machine_name' => $machine_name]), + ]; + if ($settings['default'] != $display_name) { + $operations['set_default'] = [ + 'title' => t('Set default'), + 'url' => Url::fromRoute('panelizer.default.select', ['machine_name' => $machine_name]), + ]; + $operations['delete'] = [ + 'title' => t('Delete'), + 'url' => Url::fromRoute('panelizer.default.delete', ['machine_name' => $machine_name]), + ]; + } + $form['panelizer']['displays'][$machine_name] = [ + 'label' => ['#markup' => $panels_display['label']], + 'default' => ['#markup' => $settings['default'] == $display_name ? 'TRUE' : 'FALSE'], + 'operations' => [ + 'data' => [ + '#type' => 'operations', + '#links' => $operations, + ] + ] + ]; + } + $form['fields']['#access'] = FALSE; + } $form['panelizer']['enable'] = [ '#type' => 'checkbox', '#title' => t('Panelize this view mode'), - '#default_value' => !empty($settings['enable']), + '#default_value' => isset($settings['enable']) ? $settings['enable'] : FALSE, ]; + $form['panelizer']['custom'] = [ '#type' => 'checkbox', '#title' => t('Allow custom overrides of each entity'), - '#default_value' => !empty($settings['custom']), + '#default_value' => isset($settings['custom']) ? $settings['custom'] : FALSE, + '#states' => [ + 'visible' => [ + '#edit-panelizer-enable' => ['checked' => TRUE], + ], + ], + ]; + $form['panelizer']['allow'] = [ + '#type' => 'checkbox', + '#title' => t('Allow panelizer default choice'), + '#description' => t('When multiple defaults for a view mode are available it can be useful to allow content creators to choose which default to use for a given node.'), + '#default_value' => isset($settings['allow']) ? $settings['allow'] : FALSE, '#states' => [ 'visible' => [ - ':input[name="panelizer[enable]"]' => ['checked' => TRUE], - ] - ] + '#edit-panelizer-enable' => ['checked' => TRUE], + ], + ], ]; $form['#attached']['library'][] = 'panelizer/panelizer_default_form'; - - $form['actions']['submit']['#submit'][] = 'panelizer_form_entity_view_display_edit_form_submit'; + $form['actions']['submit']['#submit'][] = 'panelizer_form_entity_view_display_edit_submit'; } } -/** - * Form submission callback for entity_view_display_edit_form. - */ -function panelizer_form_entity_view_display_edit_form_submit($form, FormStateInterface $form_state) { +function panelizer_form_entity_view_display_edit_submit(&$form, FormStateInterface $form_state) { + $rebuild = FALSE; /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ $form_object = $form_state->getFormObject(); /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ $display = $form_object->getEntity(); - - $settings = [ - 'enable' => $form_state->getValue(['panelizer', 'enable']), - 'custom' => $form_state->getValue(['panelizer', 'custom']), - ]; - - /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + /** @var \Drupal\panelizer\Plugin\PanelizerEntityManager $panelizer_manager */ + $panelizer_manager = \Drupal::service('plugin.manager.panelizer_entity'); + /** @var \Drupal\panelizer\Panelizer $panelizer */ $panelizer = \Drupal::service('panelizer'); - $panelizer->setPanelizerSettings($display->getTargetEntityTypeId(), $display->getTargetBundle(), $display->getMode(), $settings, $display); + + if ($panelizer_manager->hasDefinition($display->getTargetEntityTypeId())) { + $settings = $panelizer->getPanelizerSettings($display->getTargetEntityTypeId(), $display->getTargetBundle(), $display->getMode(), $display); + if ($settings['enable'] != $form_state->getValue(['panelizer', 'enable'])) { + $rebuild = TRUE; + } + $settings['enable'] = $form_state->getValue(['panelizer', 'enable']); + $settings['custom'] = $form_state->getValue(['panelizer', 'custom']); + $settings['allow'] = $form_state->getValue(['panelizer', 'allow']); + $panelizer->setPanelizerSettings($display->getTargetEntityTypeId(), $display->getTargetBundle(), $display->getMode(), $settings, $display); + if ($rebuild) { + \Drupal::service('router.builder')->rebuild(); + /** @var \Drupal\Core\Menu\LocalActionManager $local_action_manager */ + $local_action_manager = \Drupal::service('plugin.manager.menu.local_action'); + $local_action_manager->clearCachedDefinitions(); + // Manually reinitialize these. + $local_action_manager->getDefinitions(); + \Drupal::service('cache.render')->invalidateAll(); + } + } } /** diff --git a/panelizer.routing.yml b/panelizer.routing.yml index feac328..04189a6 100644 --- a/panelizer.routing.yml +++ b/panelizer.routing.yml @@ -12,3 +12,79 @@ panelizer.panels_ipe.revert_to_default: _method: 'POST' _permission: 'access panels in-place editing' _custom_access: '\Drupal\panelizer\Controller\PanelizerPanelsIPEController::accessRevertToDefault' + +# Wizard +panelizer.wizard.add: + path: '/admin/structure/panelizer/add/{entity_type_id}/{bundle}/{view_mode_name}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerAddWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + requirements: + _panelizer_default_access: 'TRUE' + _permission: 'administer panelizer' + +panelizer.wizard.add.step: + path: '/admin/structure/panelizer/add/{machine_name}/{step}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerAddWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.edit: + path: '/admin/structure/panelizer/edit/{machine_name}/{step}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerEditWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + step: 'general' + requirements: + _permission: 'administer panelizer' + +panelizer.default.delete: + path: '/admin/structure/panelizer/delete/{machine_name}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerDefaultDelete' + _title: 'Delete panelizer default' + requirements: + _panelizer_field_ui_view_mode_access: 'TRUE' + _custom_access: '\Drupal\panelizer\Access\PanelizerDefaultsDisplayAccess::isNotDefaultDisplay' + +panelizer.default.select: + path: '/admin/structure/panelizer/set_default/{machine_name}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerDefaultSelect' + _title: 'Set as default' + requirements: + _panelizer_field_ui_view_mode_access: 'TRUE' + _custom_access: '\Drupal\panelizer\Access\PanelizerDefaultsDisplayAccess::isNotDefaultDisplay' + +# Contexts +panelizer.wizard.step.context.add: + path: '/admin/panelizer/wizard/{machine_name}/contexts/add/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure' + _title: 'Add custom context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.step.context.edit: + path: '/admin/panelizer/wizard/{machine_name}/contexts/edit/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure' + _title: 'Edit context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.step.context.delete: + path: '/admin/panelizer/wizard/{machine_name}/context/delete/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextDeleteForm' + _title: 'Delete static context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' diff --git a/panelizer.services.yml b/panelizer.services.yml index 20c1b5d..d36b86c 100644 --- a/panelizer.services.yml +++ b/panelizer.services.yml @@ -1,7 +1,17 @@ services: + access_check.panelizer.view_mode: + class: Drupal\panelizer\Access\ViewModeAccessCheck + arguments: ['@access_check.field_ui.view_mode'] + tags: + - { name: access_check, applies_to: _panelizer_field_ui_view_mode_access } plugin.manager.panelizer_entity: class: Drupal\panelizer\Plugin\PanelizerEntityManager parent: default_plugin_manager panelizer: class: Drupal\panelizer\Panelizer - arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_field.manager', '@plugin.manager.field.field_type', '@module_handler', '@current_user', '@plugin.manager.panelizer_entity', '@panels.display_manager', '@string_translation'] + arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_field.manager', '@plugin.manager.field.field_type', '@module_handler', '@current_user', '@plugin.manager.panelizer_entity', '@panels.display_manager', '@string_translation', '@ctools.context_mapper'] + panelizer.default.access: + class: Drupal\panelizer\Access\DefaultAccess + arguments: ['@panelizer'] + tags: + - { name: access_check, applies_to: _panelizer_default_access } diff --git a/src/Access/DefaultAccess.php b/src/Access/DefaultAccess.php new file mode 100644 index 0000000..54b56c5 --- /dev/null +++ b/src/Access/DefaultAccess.php @@ -0,0 +1,50 @@ +panelizer = $panelizer; + } + + /** + * Determines access to a default Panelizer layout. + * + * @param string $entity_type_id + * The panelized entity type ID. + * @param string $bundle + * The panelized bundle ID. + * @param string $view_mode_name + * The panelized view mode ID. + * + * @return \Drupal\Core\Access\AccessResult + */ + public function access($entity_type_id, $bundle, $view_mode_name) { + $settings = $this->panelizer->getPanelizerSettings($entity_type_id, $bundle, $view_mode_name); + return $settings['enable'] ? AccessResult::allowed() : AccessResult::forbidden(); + } + +} diff --git a/src/Access/PanelizerDefaultsDisplayAccess.php b/src/Access/PanelizerDefaultsDisplayAccess.php new file mode 100644 index 0000000..f29b193 --- /dev/null +++ b/src/Access/PanelizerDefaultsDisplayAccess.php @@ -0,0 +1,39 @@ +getPanelizerSettings($entity_type, $bundle, $view_mode); + if ($settings['default'] != $default) { + $access = AccessResult::allowed(); + } + else { + $access = AccessResult::forbidden(); + } + return $access->addCacheTags(["panelizer_default:$entity_type:$bundle:$view_mode", "panelizer_default:$entity_type:$bundle:$view_mode:$default"]); + } + +} diff --git a/src/Access/PanelizerUIAccess.php b/src/Access/PanelizerUIAccess.php new file mode 100644 index 0000000..e3cb1e7 --- /dev/null +++ b/src/Access/PanelizerUIAccess.php @@ -0,0 +1,20 @@ +hasPermission('administer panelizer') ? AccessResult::allowed() : AccessResult::forbidden(); + } + +} diff --git a/src/Access/ViewModeAccessCheck.php b/src/Access/ViewModeAccessCheck.php new file mode 100644 index 0000000..db0ff21 --- /dev/null +++ b/src/Access/ViewModeAccessCheck.php @@ -0,0 +1,66 @@ +accessCheck = $access_check; + } + + /** + * Adapt the panelizer defaults access check to correspond to field ui. + * + * @param \Symfony\Component\Routing\Route $route + * The original route definition. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route matched. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user's account. + * @param string $machine_name + * The machine name of the panelizer default. + * + * @return \Drupal\Core\Access\AccessResultInterface + * @throws \Exception + */ + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $machine_name) { + $parts = explode('__', $machine_name); + if (count($parts) != 4) { + throw new \Exception('The provided machine_name is not well formed.'); + } + list($entity_type_id, $bundle, $view_mode) = $parts; + $defaults = [ + 'entity_type_id' => $entity_type_id, + ] + $route->getDefaults(); + $route->setDefaults($defaults); + $route->setRequirement('_field_ui_view_mode_access', 'administer ' . $entity_type_id . ' display'); + return $this->accessCheck->access($route, $route_match, $account, $view_mode, $bundle); + } + +} diff --git a/src/Controller/PanelizerPanelsIPEController.php b/src/Controller/PanelizerPanelsIPEController.php index d33ed64..7cebed9 100644 --- a/src/Controller/PanelizerPanelsIPEController.php +++ b/src/Controller/PanelizerPanelsIPEController.php @@ -57,16 +57,26 @@ class PanelizerPanelsIPEController extends ControllerBase { * The entity. * @param string $view_mode * The view mode. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request. * * @return \Symfony\Component\HttpFoundation\Response * An empty response. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException */ - public function revertToDefault(FieldableEntityInterface $entity, $view_mode, Request $request) { - $default = $request->get('default'); + public function revertToDefault(FieldableEntityInterface $entity, $view_mode) { + // Get the bundle specific default display as a fallback. + $settings = $this->panelizer->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + $default = $settings['default']; + // Check the entity for a documented default to which we should revert. + if ($entity->hasField('panelizer') && $entity->panelizer->first()) { + foreach ($entity->panelizer as $item) { + if ($item->view_mode == $view_mode && !empty($item->default)) { + $default = $item->default; + break; + } + } + } + // If we somehow ended up not having a default, throw an exception. if (empty($default)) { throw new BadRequestHttpException("Default name to revert to must be passed!"); } diff --git a/src/Form/PanelizerDefaultDelete.php b/src/Form/PanelizerDefaultDelete.php new file mode 100644 index 0000000..9fbedf4 --- /dev/null +++ b/src/Form/PanelizerDefaultDelete.php @@ -0,0 +1,172 @@ +entityTypeManager = $entity_type_manager; + $this->panelizer = $panelizer; + $this->panelsDisplayManager = $panels_display_manager; + $this->invalidator = $invalidator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('panelizer'), + $container->get('panels.display_manager'), + $container->get('cache_tags.invalidator') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return 'Are you certain you want to delete this panelizer default?.'; + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $bundle_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId)->getBundleEntityType(); + if ($this->viewMode == 'default') { + $route = "entity.entity_view_display.{$this->entityTypeId}.default"; + $arguments = [ + $bundle_entity_type => $this->bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$this->entityTypeId}.view_mode"; + $arguments = [ + $bundle_entity_type => $this->bundle, + 'view_mode_name' => $this->viewMode, + ]; + } + return new Url($route, $arguments); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_default_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) { + list ( + $this->entityTypeId, + $this->bundle, + $this->viewMode, + $this->displayId + ) = explode('__', $machine_name); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $display = $this->panelizer->getEntityViewDisplay($this->entityTypeId, $this->bundle, $this->viewMode); + $displays = $this->panelizer->getDefaultPanelsDisplays($this->entityTypeId, $this->bundle, $this->viewMode, $display); + unset($displays[$this->displayId]); + foreach ($displays as $key => $value) { + $displays[$key] = $this->panelsDisplayManager->exportDisplay($value); + } + $display->setThirdPartySetting('panelizer', 'displays', $displays); + $display->save(); + $form_state->setRedirectUrl($this->getCancelUrl()); + $tag = "panelizer_default:{$this->entityTypeId}:{$this->bundle}:{$this->viewMode}:{$this->displayId}"; + $this->invalidator->invalidateTags([$tag]); + } + +} diff --git a/src/Form/PanelizerDefaultSelect.php b/src/Form/PanelizerDefaultSelect.php new file mode 100644 index 0000000..2558c90 --- /dev/null +++ b/src/Form/PanelizerDefaultSelect.php @@ -0,0 +1,144 @@ +panelizer = $panelizer; + $this->invalidator = $invalidator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('panelizer'), + $container->get('cache_tags.invalidator') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return 'Are you certain you want to set this panelizer default as the default for this bundle?.'; + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $bundle_entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId)->getBundleEntityType(); + if ($this->viewMode == 'default') { + $route = "entity.entity_view_display.{$this->entityTypeId}.default"; + $arguments = [ + $bundle_entity_type => $this->bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$this->entityTypeId}.view_mode"; + $arguments = [ + $bundle_entity_type => $this->bundle, + 'view_mode_name' => $this->viewMode, + ]; + } + return new Url($route, $arguments); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_default_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) { + list ( + $this->entityTypeId, + $this->bundle, + $this->viewMode, + $this->displayId + ) = explode('__', $machine_name); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $display = $this->panelizer->getEntityViewDisplay($this->entityTypeId, $this->bundle, $this->viewMode); + $settings = $this->panelizer->getPanelizerSettings($this->entityTypeId, $this->bundle, $this->viewMode, $display); + $settings['default'] = $this->displayId; + $this->panelizer->setPanelizerSettings($this->entityTypeId, $this->bundle, $this->viewMode, $settings, $display); + $form_state->setRedirectUrl($this->getCancelUrl()); + $tag = "panelizer_default:{$this->entityTypeId}:{$this->bundle}:{$this->viewMode}"; + $this->invalidator->invalidateTags([$tag]); + } + +} diff --git a/src/Form/PanelizerWizardContextConfigure.php b/src/Form/PanelizerWizardContextConfigure.php new file mode 100644 index 0000000..7dea5d6 --- /dev/null +++ b/src/Form/PanelizerWizardContextConfigure.php @@ -0,0 +1,94 @@ +contextMapper = $context_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore'), + $container->get('ctools.context_mapper') + ); + } + + /** + * {@inheritdoc} + */ + protected function getParentRouteInfo($cached_values) { + return ['panelizer.wizard.add.step', [ + 'machine_name' => $cached_values['id'], + 'step' => 'contexts', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + $static_contexts = isset($cached_values['contexts']) ? $cached_values['contexts'] : []; + $static_contexts = $this->contextMapper->getContextValues($static_contexts); + return $static_contexts; + } + + /** + * {@inheritdoc} + */ + protected function addContext($cached_values, $context_id, ContextInterface $context) { + $cached_values['contexts'][$context_id] = [ + 'label' => $context->getContextDefinition()->getLabel(), + 'type' => $context->getContextDefinition()->getDataType(), + 'description' => $context->getContextDefinition()->getDescription(), + 'value' => strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0 ? $context->getContextValue()->uuid() : $context->getContextValue(), + ]; + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function contextExists($value, $element, $form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function disableMachineName($cached_values, $machine_name) { + return !empty($cached_values['contexts'][$machine_name]); + } + +} diff --git a/src/Form/PanelizerWizardContextDeleteForm.php b/src/Form/PanelizerWizardContextDeleteForm.php new file mode 100644 index 0000000..84c6404 --- /dev/null +++ b/src/Form/PanelizerWizardContextDeleteForm.php @@ -0,0 +1,59 @@ +getTempstore(); + $context = $cached_values['contexts'][$this->context_id]; + return $this->t('Are you sure you want to delete the context @label?', ['@label' => $context['label']]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $cached_values = $this->getTempstore(); + + return new Url('panelizer.wizard.add.step', [ + 'machine_name' => $cached_values['id'], + 'step' => 'contexts', + ]); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->getTempstore(); + $context = $cached_values['contexts'][$this->context_id]; + drupal_set_message($this->t('The static context %label has been removed.', ['%label' => $context['label']])); + unset($cached_values['contexts'][$this->context_id]); + $this->setTempstore($cached_values); + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Form/PanelizerWizardContextForm.php b/src/Form/PanelizerWizardContextForm.php new file mode 100644 index 0000000..866829e --- /dev/null +++ b/src/Form/PanelizerWizardContextForm.php @@ -0,0 +1,152 @@ +get('typed_data_manager'), + $container->get('form_builder'), + $container->get('user.shared_tempstore') + ); + } + + /** + * ManageContext constructor. + * + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + * @param \Drupal\user\SharedTempStoreFactory $tempstore_factory + * Shared user tempstore factory. + */ + public function __construct(TypedDataManagerInterface $typed_data_manager, FormBuilderInterface $form_builder, SharedTempStoreFactory $tempstore_factory) { + parent::__construct($typed_data_manager, $form_builder); + $this->tempstoreFactory = $tempstore_factory; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_wizard_context_form'; + } + + /** + * {@inheritdoc} + */ + protected function getContextClass($cached_values) { + return PanelizerWizardContextConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipClass($cached_values) {} + + /** + * {@inheritdoc} + */ + protected function getContextAddRoute($cached_values) { + return 'panelizer.wizard.step.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipAddRoute($cached_values) { + return 'panelizer.wizard.step.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + return $cached_values['plugin']->getPattern()->getDefaultContexts($this->tempstoreFactory, $this->getTempstoreId(), $this->machine_name); + } + + /** + * {@inheritdoc} + */ + protected function getTempstoreId() { + return 'panelizer.wizard'; + } + + /** + * {@inheritdoc} + */ + protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row) { + return ['panelizer.wizard.step.context', [ + 'machine_name' => $machine_name, + 'context_id' => $row, + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row) { + return ['panelizer.wizard.step.context', [ + 'machine_name' => $machine_name, + 'context_id' => $row, + ]]; + } + + /** + * {@inheritdoc} + */ + protected function isEditableContext($cached_values, $row) { + $context = $cached_values['contexts'][$row]; + return !empty($context['value']); + } + + /** + * {@inheritdoc} + */ + public function addContext(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $context = $form_state->getValue('context'); + $content = $this->formBuilder->getForm($this->getContextClass($cached_values), $context, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700'))); + return $response; + } + +} diff --git a/src/Form/PanelizerWizardGeneralForm.php b/src/Form/PanelizerWizardGeneralForm.php new file mode 100644 index 0000000..a33fc81 --- /dev/null +++ b/src/Form/PanelizerWizardGeneralForm.php @@ -0,0 +1,67 @@ +machine_name = $machine_name; + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $form['variant_settings'] = $plugin->buildConfigurationForm([], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + $form['variant_settings']['#tree'] = TRUE; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if ($form_state->hasValue('id') && !isset($this->machine_name) && $form_state->has('machine_name_prefix')) { + $form_state->setValue('id', "{$form_state->get('machine_name_prefix')}__{$form_state->getValue('id')}"); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $plugin->submitConfigurationForm($form['variant_settings'], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + $configuration = $plugin->getConfiguration(); + $cached_values['plugin']->setConfiguration($configuration); + } + +} diff --git a/src/Menu/AddDefaultLocalAction.php b/src/Menu/AddDefaultLocalAction.php new file mode 100644 index 0000000..932ed28 --- /dev/null +++ b/src/Menu/AddDefaultLocalAction.php @@ -0,0 +1,25 @@ +pluginDefinition['route_parameters']['entity_type_id'] = $route_match->getCurrentRouteMatch()->getParameter('entity_type_id'); + $this->pluginDefinition['route_parameters']['bundle'] = $route_match->getCurrentRouteMatch()->getParameter('bundle'); + $this->pluginDefinition['route_parameters']['view_mode_name'] = $route_match->getCurrentRouteMatch()->getParameter('view_mode_name'); + return parent::getRouteParameters($route_match); + } + +} diff --git a/src/Panelizer.php b/src/Panelizer.php index 864191d..67afd8f 100644 --- a/src/Panelizer.php +++ b/src/Panelizer.php @@ -20,6 +20,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\ctools\ContextMapperInterface; use Drupal\panelizer\Exception\PanelizerException; use Drupal\panelizer\Plugin\PanelizerEntityManager; use Drupal\panels\PanelsDisplayManagerInterface; @@ -89,6 +90,13 @@ class Panelizer implements PanelizerInterface { protected $panelsManager; /** + * The context mapper. + * + * @var \Drupal\ctools\ContextMapperInterface + */ + protected $contextMapper; + + /** * Constructs a Panelizer. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager @@ -109,8 +117,10 @@ class Panelizer implements PanelizerInterface { * The Panels display manager. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation service. + * @param \Drupal\ctools\ContextMapperInterface $context_mapper + * The context mapper service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ModuleHandlerInterface $module_handler, AccountProxyInterface $current_user, PanelizerEntityManager $panelizer_entity_manager, PanelsDisplayManagerInterface $panels_manager, TranslationInterface $string_translation) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ModuleHandlerInterface $module_handler, AccountProxyInterface $current_user, PanelizerEntityManager $panelizer_entity_manager, PanelsDisplayManagerInterface $panels_manager, TranslationInterface $string_translation, ContextMapperInterface $context_mapper) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->entityFieldManager = $entity_field_manager; @@ -120,6 +130,7 @@ class Panelizer implements PanelizerInterface { $this->panelizerEntityManager = $panelizer_entity_manager; $this->panelsManager = $panels_manager; $this->stringTranslation = $string_translation; + $this->contextMapper = $context_mapper; } /** @@ -135,19 +146,9 @@ class Panelizer implements PanelizerInterface { } /** - * Gets the entity view display for the entity type, bundle and view mode. - * - * @param $entity_type_id - * The entity type id. - * @param $bundle - * The bundle. - * @param $view_mode - * The view mode. - * - * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|NULL - * The entity view display if one exists; NULL otherwise. + * {@inheritdoc} */ - protected function getEntityViewDisplay($entity_type_id, $bundle, $view_mode) { + public function getEntityViewDisplay($entity_type_id, $bundle, $view_mode) { // Check the existence and status of: // - the display for the view mode, // - the 'default' display. @@ -200,7 +201,7 @@ class Panelizer implements PanelizerInterface { */ public function getPanelsDisplay(FieldableEntityInterface $entity, $view_mode, EntityViewDisplayInterface $display = NULL) { $settings = $this->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); - if ($settings['custom'] && isset($entity->panelizer)) { + if (($settings['custom'] || $settings['allow']) && isset($entity->panelizer) && $entity->panelizer->first()) { /** @var \Drupal\Core\Field\FieldItemInterface[] $values */ $values = []; foreach ($entity->panelizer as $item) { @@ -208,16 +209,68 @@ class Panelizer implements PanelizerInterface { } if (isset($values[$view_mode])) { $panelizer_item = $values[$view_mode]; + // Check for a customized display first and use that if present. + if (!empty($panelizer_item->panels_display)) { + // @todo: validate schema after https://www.drupal.org/node/2392057 is fixed. + return $this->panelsManager->importDisplay($panelizer_item->panels_display, FALSE); + } + // If not customized, use the specified default. if (!empty($panelizer_item->default)) { - return $this->getDefaultPanelsDisplay($panelizer_item->default, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + // If we're using this magic key use the settings default. + if ($panelizer_item->default == '__bundle_default__') { + $default = $settings['default']; + } + else { + $default = $panelizer_item->default; + // Ensure the default still exists and if not fallback sanely. + $displays = $this->getDefaultPanelsDisplays($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + if (!isset($displays[$default])) { + $default = $settings['default']; + } + } + $panels_display = $this->getDefaultPanelsDisplay($default, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + $this->setCacheTags($panels_display, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display, $default, $settings); + return $panels_display; } - - // @todo: validate schema after https://www.drupal.org/node/2392057 is fixed. - return $this->panelsManager->importDisplay($panelizer_item->panels_display, FALSE); } } + // If the field has no input to give us, use the settings default. + $panels_display = $this->getDefaultPanelsDisplay($settings['default'], $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + $this->setCacheTags($panels_display, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display, $settings['default'], $settings); + return $panels_display; + } - return $this->getDefaultPanelsDisplay('default', $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + /** + * Properly determine the cache tags for a display and set them. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The panels display variant. + * @param string $entity_type_id + * The entity type id. + * @param string $bundle + * The bundle. + * @param string $view_mode + * The view mode. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface|NULL $display + * If the caller already has the correct display, it can optionally be + * passed in here so the Panelizer service doesn't have to look it up; + * otherwise, this argument can be omitted. + * @param $default + * The name of the panels display we are about to render. + * @param array $settings + * The default panelizer settings for this EntityViewDisplay. + */ + protected function setCacheTags(PanelsDisplayVariant $panels_display, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL, $default, array $settings) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + $display_mode = $display ? $display->getMode() : ''; + + if ($default == $settings['default']) { + $tags = ["{$panels_display->getStorageType()}:{$entity_type_id}:{$bundle}:{$display_mode}"]; + } + $tags[] = "{$panels_display->getStorageType()}:{$entity_type_id}:{$bundle}:{$display_mode}:$default"; + $panels_display->addCacheTags($tags); } /** @@ -225,7 +278,7 @@ class Panelizer implements PanelizerInterface { */ public function setPanelsDisplay(FieldableEntityInterface $entity, $view_mode, $default, PanelsDisplayVariant $panels_display = NULL) { $settings = $this->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode); - if ($settings['custom'] && isset($entity->panelizer)) { + if (($settings['custom'] || $settings['allow']) && isset($entity->panelizer)) { $panelizer_item = NULL; /** @var \Drupal\Core\Field\FieldItemInterface $item */ foreach ($entity->panelizer as $item) { @@ -303,6 +356,8 @@ class Panelizer implements PanelizerInterface { $config = $display->getThirdPartySetting('panelizer', 'displays', []); if (!empty($config[$name])) { + // Set a default just in case. + $config[$name]['builder'] = empty($config[$name]['builder']) ? 'standard' : $config[$name]['builder']; // @todo: validate schema after https://www.drupal.org/node/2392057 is fixed. $panels_display = $this->panelsManager->importDisplay($config[$name], FALSE); } @@ -342,6 +397,43 @@ class Panelizer implements PanelizerInterface { /** * {@inheritdoc} */ + public function getDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + // If we still don't find a display, then we won't find a Panelizer + // default for sure. + if (!$display) { + return NULL; + } + } + + $config = $display->getThirdPartySetting('panelizer', 'displays', []); + if (!empty($config[$name]) && !empty($config[$name]['static_context'])) { + return $this->contextMapper->getContextValues($config[$name]['static_context']); + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, $contexts) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + if (!$display) { + throw new PanelizerException("Unable to find display for given entity type, bundle and view mode"); + } + + // Set this Panels display's static contexts. + $panels_displays = $display->getThirdPartySetting('panelizer', 'displays', []); + $panels_displays[$name]['static_context'] = $contexts; + $display->setThirdPartySetting('panelizer', 'displays', $panels_displays); + + $display->save(); + } + + /** + * {@inheritdoc} + */ public function isPanelized($entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { if (!$this->getEntityPlugin($entity_type_id)) { return FALSE; @@ -365,6 +457,8 @@ class Panelizer implements PanelizerInterface { $settings = [ 'enable' => $this->isPanelized($entity_type_id, $bundle, $view_mode, $display), 'custom' => $display->getThirdPartySetting('panelizer', 'custom', FALSE), + 'allow' => $display->getThirdPartySetting('panelizer', 'allow', FALSE), + 'default' => $display->getThirdPartySetting('panelizer', 'default', 'default'), ]; // Make sure that the Panelizer field actually exists. @@ -386,6 +480,8 @@ class Panelizer implements PanelizerInterface { $display->setThirdPartySetting('panelizer', 'enable', !empty($settings['enable'])); $display->setThirdPartySetting('panelizer', 'custom', !empty($settings['enable']) && !empty($settings['custom'])); + $display->setThirdPartySetting('panelizer', 'allow', !empty($settings['enable']) && !empty($settings['allow'])); + $display->setThirdPartySetting('panelizer', 'default', $settings['default']); if (!empty($settings['enable'])) { // Set the default display. @@ -394,11 +490,12 @@ class Panelizer implements PanelizerInterface { /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_entity_plugin */ $panelizer_entity_plugin = $this->panelizerEntityManager->createInstance($display->getTargetEntityTypeId(), []); $displays['default'] = $this->panelsManager->exportDisplay($panelizer_entity_plugin->getDefaultDisplay($display, $display->getTargetBundle(), $display->getMode())); + $settings['default'] = "{$display->getTargetEntityTypeId()}__{$display->getTargetBundle()}__{$view_mode}__default"; $display->setThirdPartySetting('panelizer', 'displays', $displays); } // Make sure the field exists. - if (!empty($settings['custom'])) { + if (($settings['custom'] || $settings['allow'])) { $field_storage = $this->entityTypeManager->getStorage('field_storage_config')->load($entity_type_id . '.panelizer'); if (!$field_storage) { $field_storage = $this->entityTypeManager->getStorage('field_storage_config')->create([ @@ -564,4 +661,4 @@ class Panelizer implements PanelizerInterface { return $this->hasOperationPermission($op, $entity_type_id, $bundle, $account); } -} \ No newline at end of file +} diff --git a/src/PanelizerEntityViewBuilder.php b/src/PanelizerEntityViewBuilder.php index 3a15689..b5cd6f3 100644 --- a/src/PanelizerEntityViewBuilder.php +++ b/src/PanelizerEntityViewBuilder.php @@ -352,6 +352,8 @@ class PanelizerEntityViewBuilder implements EntityViewBuilderInterface, EntityHa foreach ($entities as $id => $entity) { $panels_display = $this->panelizer->getPanelsDisplay($entity, $view_mode, $displays[$entity->bundle()]); + $settings = $this->panelizer->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode, $displays[$entity->bundle()]); + $panels_display->setContexts($this->panelizer->getDisplayStaticContexts($settings['default'], $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $displays[$entity->bundle()])); $build[$id] = $this->buildPanelized($entity, $panels_display, $view_mode, $langcode); } @@ -393,8 +395,8 @@ class PanelizerEntityViewBuilder implements EntityViewBuilderInterface, EntityHa } // @todo: I'm sure more is necessary to get the cache contexts right... - CacheableMetadata::createFromObject($entity) - ->applyTo($build); + $entity_metadata = CacheableMetadata::createFromObject($entity); + CacheableMetadata::createFromObject($panels_display)->merge($entity_metadata)->applyTo($build); $this->getPanelizerPlugin()->alterBuild($build, $entity, $panels_display, $view_mode); diff --git a/src/PanelizerInterface.php b/src/PanelizerInterface.php index 0372277..a793984 100644 --- a/src/PanelizerInterface.php +++ b/src/PanelizerInterface.php @@ -20,6 +20,21 @@ use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant; interface PanelizerInterface { /** + * Gets the entity view display for the entity type, bundle and view mode. + * + * @param $entity_type_id + * The entity type id. + * @param $bundle + * The bundle. + * @param $view_mode + * The view mode. + * + * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|NULL + * The entity view display if one exists; NULL otherwise. + */ + public function getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + + /** * Gets the Panels display for a given entity and view mode. * * @param \Drupal\Core\Entity\EntityInterface $entity @@ -115,6 +130,12 @@ interface PanelizerInterface { */ public function setDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode, PanelsDisplayVariant $panels_display); + public function getDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL); + + public function setDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, $contexts); + + + /** * Checks if the given entity type, bundle and view mode are panelized. * diff --git a/src/Plugin/AddDefaultLinkDeriver.php b/src/Plugin/AddDefaultLinkDeriver.php new file mode 100644 index 0000000..cbd6f31 --- /dev/null +++ b/src/Plugin/AddDefaultLinkDeriver.php @@ -0,0 +1,56 @@ +panelizerEntityManager = $panelizer_entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('plugin.manager.panelizer_entity') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + foreach ($this->panelizerEntityManager->getDefinitions() as $plugin_id => $definition) { + $this->derivatives["$plugin_id"] = $base_plugin_definition; + $this->derivatives["$plugin_id"]['appears_on'] = [ + "entity.entity_view_display.$plugin_id.default", + "entity.entity_view_display.$plugin_id.view_mode" + ]; + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/src/Plugin/Field/FieldType/PanelizerFieldType.php b/src/Plugin/Field/FieldType/PanelizerFieldType.php index a1627bc..7ad1032 100644 --- a/src/Plugin/Field/FieldType/PanelizerFieldType.php +++ b/src/Plugin/Field/FieldType/PanelizerFieldType.php @@ -43,7 +43,7 @@ class PanelizerFieldType extends FieldItemBase { ->setRequired(FALSE); $properties['panels_display'] = MapDataDefinition::create('map') ->setLabel(new TranslatableMarkup('Panels display')) - ->setRequired(TRUE); + ->setRequired(FALSE); return $properties; } @@ -86,6 +86,8 @@ class PanelizerFieldType extends FieldItemBase { } /** + * Returns the Panels display plugin manager. + * * @return \Drupal\panels\PanelsDisplayManagerInterface */ protected static function getPanelsDisplayManager() { @@ -120,9 +122,12 @@ class PanelizerFieldType extends FieldItemBase { public function postSave($update) { $panels_manager = $this->getPanelsDisplayManager(); $panels_display_config = $this->get('panels_display')->getValue(); - if (!empty($panels_display_config)) { - $panels_display = $panels_manager->importDisplay($panels_display_config, FALSE); + // If our field has custom panelizer display config data. + if (!empty($panels_display_config) && is_array($panels_display_config)) { + $panels_display = $panels_manager->importDisplay($panels_display_config, FALSE); + } + if (!empty($panels_display)) { // Set the storage id to include the current revision id. $entity = $this->getEntity(); $storage_id_parts = [ diff --git a/src/Plugin/Field/FieldWidget/PanelizerWidget.php b/src/Plugin/Field/FieldWidget/PanelizerWidget.php index 6a3601f..e9b8845 100644 --- a/src/Plugin/Field/FieldWidget/PanelizerWidget.php +++ b/src/Plugin/Field/FieldWidget/PanelizerWidget.php @@ -8,12 +8,9 @@ namespace Drupal\panelizer\Plugin\Field\FieldWidget; use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'panelizer' widget. @@ -30,6 +27,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class PanelizerWidget extends WidgetBase { /** + * Returns the Panels display plugin manager. + * * @return \Drupal\panels\PanelsDisplayManagerInterface */ public function getPanelsManager() { @@ -38,6 +37,8 @@ class PanelizerWidget extends WidgetBase { } /** + * Returns the Panelizer entity plugin manager. + * * @return \Drupal\panelizer\Plugin\PanelizerEntityManager */ public function getPanelizerManager() { @@ -46,50 +47,21 @@ class PanelizerWidget extends WidgetBase { } /** - * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface + * Returns the Panelizer service. + * + * @return \Drupal\panelizer\PanelizerInterface */ - public function getEntityDisplayRepository() { + public function getPanelizer() { // @todo: is it possible to inject this? - return \Drupal::service('entity_display.repository'); - } - - /** - * {@inheritdoc} - */ - public static function defaultSettings() { - return array( - 'allow_panel_choice' => FALSE, - ) + parent::defaultSettings(); - } - - /** - * {@inheritdoc} - */ - public function settingsForm(array $form, FormStateInterface $form_state) { - $elements = []; - - /* - $elements['allow_panel_choice'] = array( - '#type' => 'checkbox', - '#title' => t('Allow panel choice'), - '#default_value' => $this->getSetting('allow_panel_choice'), - ); - */ - - return $elements; + return \Drupal::service('panelizer'); } /** - * {@inheritdoc} + * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface */ - public function settingsSummary() { - $summary = []; - - if (!empty($this->getSetting('allow_panel_choice'))) { - $summary[] = t('Allow panel choice'); - } - - return $summary; + public function getEntityDisplayRepository() { + // @todo: is it possible to inject this? + return \Drupal::service('entity_display.repository'); } /** @@ -110,19 +82,18 @@ class PanelizerWidget extends WidgetBase { ]; } - /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_plugin */ - $panelizer_plugin = $this->getPanelizerManager()->createInstance($entity_type_id, []); - // If any view modes are missing, then set the default. + $displays = []; foreach ($entity_view_modes as $view_mode => $view_mode_info) { - if (!isset($values[$view_mode])) { - $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + $displays[$view_mode] = $display->getThirdPartySetting('panelizer', 'displays', []); + // If we don't have a value, or the default is __bundle_default__ and our + // panels_display is empty, set the default to __bundle_default__. + if (!isset($values[$view_mode]) || ($values[$view_mode]['default'] == '__bundle_default__' && empty($values[$view_mode]['panels_display']))) { if ($display->getThirdPartySetting('panelizer', 'enable', FALSE)) { - $panels_display = $panelizer_plugin->getDefaultDisplay($display, $entity->bundle(), $view_mode); - $values[$view_mode] = [ - 'default' => 'default', - 'panels_display' => $this->getPanelsManager()->exportDisplay($panels_display), + 'default' => '__bundle_default__', + 'panels_display' => [], ]; } } @@ -136,14 +107,28 @@ class PanelizerWidget extends WidgetBase { '#value' => $view_mode, ]; - if (!empty($this->getSetting('allow_panel_choice'))) { + $settings = $this->getPanelizer()->getPanelizerSettings($entity_type_id, $entity->bundle(), $view_mode); + if (!empty($settings['allow'])) { + // We default to this option when the user hasn't previous interacted + // with the field. + $options = [ + '__bundle_default__' => $this->t('Current default display'), + ]; + foreach ($displays[$view_mode] as $machine_name => $panels_display) { + $options[$machine_name] = $panels_display['label']; + } $element[$delta]['default'] = [ + '#title' => $entity_view_modes[$view_mode]['label'], '#type' => 'select', - // @todo: list the view mode in the title - // @todo: get the list of defaults - '#options' => [], + '#options' => $options, '#default_value' => $value['default'], ]; + // If we have a value in panels_display, prevent the user from + // interacting with the widget for the view modes that are overridden. + if (!empty($value['panels_display'])) { + $element[$delta]['default']['#disabled'] = TRUE; + $element[$delta]['default']['#options'][$value['default']] = $this->t('Custom Override'); + } } else { $element[$delta]['default'] = [ diff --git a/src/Plugin/PanelizerEntity/PanelizerNode.php b/src/Plugin/PanelizerEntity/PanelizerNode.php index f5e387c..9b9fa62 100644 --- a/src/Plugin/PanelizerEntity/PanelizerNode.php +++ b/src/Plugin/PanelizerEntity/PanelizerNode.php @@ -25,6 +25,7 @@ class PanelizerNode extends PanelizerEntityBase { public function getDefaultDisplay(EntityViewDisplayInterface $display, $bundle, $view_mode) { $panels_display = parent::getDefaultDisplay($display, $bundle, $view_mode) ->setPageTitle('[node:title]'); + $panels_display->setConfiguration(['label' => $this->t('Default')] + $panels_display->getConfiguration()); // Remove the 'title' block because it's covered already. foreach ($panels_display->getRegionAssignments() as $region => $blocks) { diff --git a/src/Plugin/PanelizerEntityBase.php b/src/Plugin/PanelizerEntityBase.php index d7ea1d6..4c06dc8 100644 --- a/src/Plugin/PanelizerEntityBase.php +++ b/src/Plugin/PanelizerEntityBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Panels\PanelsDisplayManager; use Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,6 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * Base class for Panelizer entity plugins. */ abstract class PanelizerEntityBase extends PluginBase implements PanelizerEntityInterface, ContainerFactoryPluginInterface { + use StringTranslationTrait; /** * @var \Drupal\Panels\PanelsDisplayManager @@ -71,6 +73,7 @@ abstract class PanelizerEntityBase extends PluginBase implements PanelizerEntity $panels_display->setLayout('onecol'); // @todo: For now we always use the IPE, but we should support not using the ipe. $panels_display->setBuilder('ipe'); + $panels_display->setPattern('panelizer'); // Add all the visible fields to the Panel. $entity_type_id = $this->getPluginId(); diff --git a/src/Plugin/PanelsPattern/PanelizerPattern.php b/src/Plugin/PanelsPattern/PanelizerPattern.php new file mode 100644 index 0000000..6e0eef5 --- /dev/null +++ b/src/Plugin/PanelsPattern/PanelizerPattern.php @@ -0,0 +1,33 @@ +t('Entity being panelized')); + $contexts['@panelizer.entity_context:entity'] = new Context($entity_definition); + $user_definition = new ContextDefinition("entity:user", $this->t('Current user')); + $contexts['current_user'] = new Context($user_definition); + return $contexts + parent::getDefaultContexts($tempstore, $tempstore_id, $machine_name); + } + +} diff --git a/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php b/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php index 72009c9..f2c3964 100644 --- a/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php +++ b/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php @@ -122,13 +122,20 @@ class PanelizerDefaultPanelsStorage extends PanelsStorageBase implements Contain * @param \Drupal\Core\Entity\EntityInterface|NULL $entity * The entity. * - * @return \Drupal\Core\Plugin\Context\Context - * The context. + * @return \Drupal\Core\Plugin\Context\Context[] + * The available contexts. */ protected function getEntityContext($entity_type_id, EntityInterface $entity = NULL) { - return new Context(new ContextDefinition('entity:' . $entity_type_id, NULL, TRUE), $entity); + $contexts = []; + // Set a placeholder context so that the calling code knows that we need + // an entity context. If we have the value available, then we actually set + // the context value. + $contexts['@panelizer.entity_context:entity'] = new Context(new ContextDefinition('entity:' . $entity_type_id, NULL, TRUE), $entity); + return $contexts; } + + /** * {@inheritdoc} */ @@ -136,12 +143,8 @@ class PanelizerDefaultPanelsStorage extends PanelsStorageBase implements Contain try { list ($entity_type_id, $bundle, $view_mode, $name, $entity) = $this->parseId($id); if ($panels_display = $this->panelizer->getDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode)) { - // Set a placeholder context so that the calling code knows that we need - // an entity context. If we have the value available, then we actually set - // the context value. - $contexts = [ - '@panelizer.entity_context:entity' => $this->getEntityContext($entity_type_id, $entity), - ]; + $contexts = $this->getEntityContext($entity_type_id, $entity); + $contexts = $contexts + $this->panelizer->getDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode); $panels_display->setContexts($contexts); return $panels_display; } diff --git a/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php b/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php index 7ea92c8..5259f30 100644 --- a/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php +++ b/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php @@ -134,9 +134,21 @@ class PanelizerFieldPanelsStorage extends PanelsStorageBase implements Container $id = $panels_display->getStorageId(); if ($entity = $this->loadEntity($id)) { list (,, $view_mode) = explode(':', $id); + // If we're dealing with an entity that has a documented default, we + // don't want to lose that information when we save our customizations. + // This enables us to revert to the correct default at a later date. if ($entity instanceof FieldableEntityInterface) { + $default = NULL; + if ($entity->hasField('panelizer') && $entity->panelizer->first()) { + foreach ($entity->panelizer as $item) { + if ($item->view_mode == $view_mode) { + $default = $item->default; + break; + } + } + } try { - $this->panelizer->setPanelsDisplay($entity, $view_mode, NULL, $panels_display); + $this->panelizer->setPanelsDisplay($entity, $view_mode, $default, $panels_display); } catch (PanelizerException $e) { // Translate to expected exception type. diff --git a/src/Tests/PanelizerAddDefaultLinkTest.php b/src/Tests/PanelizerAddDefaultLinkTest.php new file mode 100644 index 0000000..897a0ce --- /dev/null +++ b/src/Tests/PanelizerAddDefaultLinkTest.php @@ -0,0 +1,51 @@ +setPassword('foo')->save(); + $account->pass_raw = 'foo'; + $this->drupalLogin($account); + } + + public function test() { + $this->panelize('page'); + $this->assertLink('Add panelizer default'); + $this->unpanelize('page'); + $this->assertNoLink('Add panelizer default'); + } + +} diff --git a/src/Tests/PanelizerDefaultsTest.php b/src/Tests/PanelizerDefaultsTest.php new file mode 100644 index 0000000..a2090d7 --- /dev/null +++ b/src/Tests/PanelizerDefaultsTest.php @@ -0,0 +1,78 @@ +setPassword('foo')->save(); + $account->pass_raw = 'foo'; + $this->drupalLogin($account); + } + + public function test() { + $this->panelize('page'); + + // Create an additional default layout so we can assert that it's available + // as an option when choosing the layout on the node form. + $default_id = $this->addPanelizerDefault('page'); + $this->assertDefaultExists('page', 'default', $default_id); + + // The user should only be able to choose the layout if specifically allowed + // to (the panelizer[allow] checkbox in the view display configuration). By + // default, they aren't. + $this->drupalGet('node/add/page'); + $this->assertNoFieldByName('panelizer[0][default]'); + + // Enable layout selection and assert that all the expected fields show up. + $this->panelize('page', NULL, ['panelizer[allow]' => TRUE]); + $this->drupalGet('node/add/page'); + $view_modes = \Drupal::service('entity_display.repository')->getViewModes('node'); + $view_modes = array_filter($view_modes, function (array $view_mode) { + // View modes that are inheriting the default display (i.e., status is + // FALSE) will not show up unless they, too, are panelized. But in this + // test, we only panelized the default display. + return $view_mode['status'] == FALSE; + }); + for ($i = 0; $i < count($view_modes); $i++) { + $this->assertFieldByName("panelizer[{$i}][default]"); + $this->assertOption("edit-panelizer-{$i}-default", 'default'); + $this->assertOption("edit-panelizer-{$i}-default", $default_id); + } + + $this->deletePanelizerDefault('page', 'default', $default_id); + $this->assertDefaultNotExists('page', 'default', $default_id); + } + +} diff --git a/src/Tests/PanelizerNodeFunctionalTest.php b/src/Tests/PanelizerNodeFunctionalTest.php index 668afe6..3ffda7f 100644 --- a/src/Tests/PanelizerNodeFunctionalTest.php +++ b/src/Tests/PanelizerNodeFunctionalTest.php @@ -46,24 +46,105 @@ class PanelizerNodeFunctionalTest extends WebTestBase { $user = $this->drupalCreateUser([ 'administer node display', 'administer nodes', + 'administer pages', 'administer content types', 'create page content', + 'create article content', 'administer panelizer', 'access panels in-place editing', - 'use panels in place editing', + 'view the administration theme', ]); $this->drupalLogin($user); + } - $this->drupalPostForm('admin/structure/types/manage/page/display', [ - 'panelizer[enable]' => TRUE, - 'panelizer[custom]' => TRUE, - ], 'Save'); + /** + * Tests the admin interface to set a default layout for a bundle. + */ + public function testWizardUI() { + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->clickLink('Panelize this view mode'); + // General settings step. + $edit = [ + 'enable' => TRUE, + 'custom' => TRUE, + ]; + + // Contexts step. + $this->drupalPostForm(NULL, $edit, 'Next'); + $this->assertText('panelizer_context_entity', 'The current entity context is present.'); + + // Layout selection step. + $this->drupalPostForm(NULL, [], 'Next'); + + // Content step. Add the Node block to the top region. + $this->drupalPostForm(NULL, [], 'Next'); + $this->clickLink('Add new block'); + $this->clickLink('Title'); + $edit = [ + 'region' => 'middle', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + + // Finish the wizard. + $this->drupalPostForm(NULL, [], 'Finish'); + + // Check that the general setting was saved. + $this->assertFieldChecked('edit-custom'); + + // Now change the setting and then cancel changes. + $this->drupalPostForm(NULL, ['custom' => FALSE], 'Update'); + $this->assertNoFieldChecked('edit-custom'); + $this->drupalPostForm(NULL, [], 'Cancel'); + $this->assertFieldChecked('edit-custom'); + + // Now change and save the general setting. + $this->drupalPostForm(NULL, ['custom' => FALSE], 'Update and save'); + $this->assertNoFieldChecked('edit-custom'); + $this->drupalPostForm(NULL, [], 'Cancel'); + $this->assertNoFieldChecked('edit-custom'); + + // Add another block at the Content step and then save changes. + $this->clickLink('Content'); + $this->clickLink('Add new block'); + $this->clickLink('Body'); + $edit = [ + 'region' => 'middle', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText('entity_field:node:body', 'The body block was added successfully.'); + $this->drupalPostForm(NULL, [], 'Save'); + $this->clickLink('Content'); + $this->assertText('entity_field:node:body', 'The body block was saved successfully.'); + + // Check that the Manage Display tab changed now that Panelizer is set up. + // Also, the field display table should be hidden. + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->assertLink('This display mode is managed by Panelizer. Click here to go to its settings.'); + $this->assertNoRaw('
'); + + // Disable Panelizer for the default display mode. + // This should bring back the field overview table at Manage Display + // and make the "Panelize this view mode" link to point to the Edit + // Wizard UI. + $this->clickLink('This display mode is managed by Panelizer. Click here to go to its settings.'); + $edit = [ + 'enable' => FALSE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->assertLink('Panelize this view mode'); + $this->assertLinkByHref('admin/structure/panelizer/edit/node__article__default'); + $this->assertRaw('
'); } /** * Tests rendering a node with Panelizer default. */ public function testPanelizerDefault() { + $this->drupalPostForm('admin/structure/types/manage/page/display', [ + 'panelizer[enable]' => TRUE, + 'panelizer[custom]' => TRUE, + ], 'Save'); /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ $panelizer = \Drupal::service('panelizer'); $displays = $panelizer->getDefaultPanelsDisplays('node', 'page', 'default'); diff --git a/src/Tests/PanelizerTestTrait.php b/src/Tests/PanelizerTestTrait.php new file mode 100644 index 0000000..3170a4b --- /dev/null +++ b/src/Tests/PanelizerTestTrait.php @@ -0,0 +1,147 @@ +drupalGet("admin/structure/types/manage/{$node_type}/display/{$display}"); + + $values['panelizer[enable]'] = TRUE; + $this->drupalPostForm(NULL, $values, 'Save'); + + EntityFormDisplay::load('node.' . $node_type . '.default') + ->setComponent('panelizer', [ + 'type' => 'panelizer', + ]) + ->save(); + } + + /** + * Unpanelizes a node type's default view display. + * + * Panelizer is disabled for the display, but its configuration is retained. + * + * @param string $node_type + * The node type ID. + * @param string $display + * (optional) The view display to unpanelize. + * @param array $values + * (optional) Additional form values. + */ + protected function unpanelize($node_type, $display = NULL, array $values = []) { + $this->drupalGet("admin/structure/types/manage/{$node_type}/display/{$display}"); + + $values['panelizer[enable]'] = FALSE; + $this->drupalPostForm(NULL, $values, 'Save'); + + EntityFormDisplay::load('node.' . $node_type . '.default') + ->removeComponent('panelizer') + ->save(); + } + + protected function addPanelizerDefault($node_type, $display = 'default') { + $label = $this->getRandomGenerator()->word(16); + $id = strtolower($label); + $default_id = "node__{$node_type}__{$display}__{$id}"; + $options = array( + 'query' => array( + 'js' => 'nojs', + ), + ); + + $this->drupalGet("admin/structure/types/manage/{$node_type}/display"); + $this->clickLink('Add panelizer default'); + + // Step 1: Enter the default's label and ID. + $this->drupalPostForm(NULL, [ + 'id' => $id, + 'label' => $label, + ], 'Next'); + + // Step 2: Define contexts. + $this->assertResponse(200); + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/contexts", $options); + $this->drupalPostForm(NULL, [], 'Next'); + + // Step 3: Select layout. + $this->assertResponse(200); + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/layout", $options); + $this->drupalPostForm(NULL, [], 'Next'); + + // Step 4: Select content. + $this->assertResponse(200); + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/content", $options); + $this->drupalPostForm(NULL, [], 'Finish'); + + return $id; + } + + /** + * Deletes a Panelizer default. + * + * @param string $node_type + * The node type ID. + * @param string $display + * (optional) The view display ID. + * @param string $id + * (optional) The default ID. + */ + protected function deletePanelizerDefault($node_type, $display = 'default', $id = 'default') { + $this->drupalGet("admin/structure/panelizer/delete/node__{$node_type}__{$display}__{$id}"); + $this->drupalPostForm(NULL, [], 'Confirm'); + } + + /** + * Asserts that a Panelizer default exists. + * + * @param string $node_type + * The node type ID. + * @param string $display + * (optional) The view display ID. + * @param string $id + * (optional) The default ID. + */ + protected function assertDefaultExists($node_type, $display = 'default', $id = 'default') { + $settings = EntityViewDisplay::load("node.{$node_type}.{$display}") + ->getThirdPartySettings('panelizer'); + + $display_exists = isset($settings['displays'][$id]); + $this->assertTrue($display_exists); + } + + /** + * Asserts that a Panelizer default does not exist. + * + * @param string $node_type + * The node type ID. + * @param string $display + * The view display ID. + * @param string $id + * The default ID. + */ + protected function assertDefaultNotExists($node_type, $display = 'default', $id = 'default') { + $settings = EntityViewDisplay::load("node.{$node_type}.{$display}") + ->getThirdPartySettings('panelizer'); + + $display_exists = isset($settings['displays'][$id]); + $this->assertFalse($display_exists); + } + +} diff --git a/src/Wizard/PanelizerAddWizard.php b/src/Wizard/PanelizerAddWizard.php new file mode 100644 index 0000000..85513e8 --- /dev/null +++ b/src/Wizard/PanelizerAddWizard.php @@ -0,0 +1,72 @@ +set('machine_name_prefix', "{$entity_type_id}__{$bundle}__{$view_mode_name}"); + } + $form = parent::buildForm($form, $form_state); + $cached_values = $form_state->getTemporaryValue('wizard'); + $cached_values['id'] = $this->getMachineName(); + // Some variants like PanelsDisplayVariant need this. Set it to empty. + $cached_values['access'] = []; + $form_state->setTemporaryValue('wizard', $cached_values); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $operations = array_map('strval', [ + $this->getNextOp(), + $this->t('Update'), + $this->t('Update and save'), + $this->t('Save'), + ]); + + if (in_array($form_state->getValue('op'), $operations)) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->hasValue('label')) { + $config = $cached_values['plugin']->getConfiguration(); + $config['label'] = $form_state->getValue('label'); + $cached_values['plugin']->setConfiguration($config); + } + if ($form_state->hasValue('id')) { + $cached_values['id'] = $form_state->getValue('id'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $plugin->setStorage($plugin->getStorageType(), $cached_values['id']); + } + } + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + parent::finish($form, $form_state); + $cached_values = $form_state->getTemporaryValue('wizard'); + $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id'], 'step' => 'content']); + } + +} diff --git a/src/Wizard/PanelizerEditWizard.php b/src/Wizard/PanelizerEditWizard.php new file mode 100644 index 0000000..fd68561 --- /dev/null +++ b/src/Wizard/PanelizerEditWizard.php @@ -0,0 +1,225 @@ +getMachineName(); + list($entity_type, $bundle, $view_mode, $display_id) = explode('__', $this->getMachineName()); + $panelizer = \Drupal::service('panelizer'); + // Load the panels display variant. + /** @var \Drupal\panelizer\Panelizer $panelizer */ + // @todo this $display_id looks all wrong to me since it's the name and view_mode. + $variant_plugin = $panelizer->getDefaultPanelsDisplay($display_id, $entity_type, $bundle, $view_mode); + $cached_values['plugin'] = $variant_plugin; + $cached_values['label'] = $cached_values['plugin']->getConfiguration()['label']; + + $display = $panelizer->getEntityViewDisplay($entity_type, $bundle, $view_mode); + $config = $display->getThirdPartySetting('panelizer', 'displays', []); + if (!empty($config[$display_id]['static_context'])) { + $cached_values['contexts'] = $config[$display_id]['static_context']; + } + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + // The page actions. + $form['wizard_actions'] = [ + '#theme' => 'links', + '#links' => [], + '#attributes' => [ + 'class' => ['inline'], + ] + ]; + + // The tree of wizard steps. + $form['wizard_tree'] = [ + '#theme' => ['panelizer_wizard_tree'], + '#wizard' => $this, + '#cached_values' => $form_state->getTemporaryValue('wizard'), + ]; + + $form['#theme'] = 'panelizer_wizard_form'; + $form['#attached']['library'][] = 'panelizer/wizard_admin'; + $form = parent::customizeForm($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(FormInterface $form_object, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $operation = $this->getOperation($cached_values); + + $actions = []; + + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['update_and_save'] = [ + '#type' => 'submit', + '#value' => $this->t('Update and save'), + '#button_type' => 'primary', + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['finish'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['cancel'] = [ + '#type' => 'submit', + '#value' => $this->t('Cancel'), + '#submit' => [ + '::clearTempstore' + ], + ]; + + // Add any submit or validate functions for the step and the global ones. + foreach (['submit', 'update_and_save', 'finish'] as $button) { + if (isset($operation['validate'])) { + $actions[$button]['#validate'] = array_merge($actions[$button]['#validate'], $operation['validate']); + } + $actions[$button]['#validate'][] = '::validateForm'; + if (isset($operation['submit'])) { + $actions[$button]['#submit'] = array_merge($actions[$button]['#submit'], $operation['submit']); + } + $actions[$button]['#submit'][] = '::submitForm'; + } + $actions['update_and_save']['#submit'][] = '::finish'; + $actions['finish']['#submit'][] = '::finish'; + + if ($form_state->get('ajax')) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $ajax_parameters = $this->getNextParameters($cached_values); + $ajax_parameters['step'] = $this->getStep($cached_values); + $ajax_url = Url::fromRoute($this->getRouteName(), $ajax_parameters); + $ajax_options = [ + 'query' => $this->getRequest()->query->all() + [ + FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, + ], + ]; + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxSubmit', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + $actions['update_and_save']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + $actions['finish']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $operations = array_map('strval', [ + $this->getNextOp(), + $this->t('Update'), + $this->t('Update and save'), + $this->t('Save'), + ]); + + if (in_array($form_state->getValue('op'), $operations)) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->hasValue('label')) { + $config = $cached_values['plugin']->getConfiguration(); + $config['label'] = $form_state->getValue('label'); + $cached_values['plugin']->setConfiguration($config); + } + if ($form_state->hasValue('id')) { + $cached_values['id'] = $form_state->getValue('id'); + } + if (is_null($this->machine_name) && !empty($cached_values['id'])) { + $this->machine_name = $cached_values['id']; + } + $this->getTempstore()->set($this->getMachineName(), $cached_values); + if (!$form_state->get('ajax')) { + $form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values)); + } + } + } + + /** + * Clears the temporary store. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function clearTempstore(array &$form, FormStateInterface $form_state) { + $this->getTempstore()->delete($this->getMachineName()); + list($entity_type_id, $bundle, $view_mode) = explode('__', $this->getMachineName()); + $bundle_entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getBundleEntityType(); + if ($view_mode == 'default') { + $route = "entity.entity_view_display.{$entity_type_id}.default"; + $arguments = [ + $bundle_entity_type => $bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$entity_type_id}.view_mode"; + $arguments = [ + $bundle_entity_type => $bundle, + 'view_mode_name' => $view_mode, + ]; + } + $form_state->setRedirect($route, $arguments); + } + +} diff --git a/src/Wizard/PanelizerWizardBase.php b/src/Wizard/PanelizerWizardBase.php new file mode 100644 index 0000000..7fc42c7 --- /dev/null +++ b/src/Wizard/PanelizerWizardBase.php @@ -0,0 +1,156 @@ +getTemporaryValue('wizard'); + // Get the current form operation. + $operation = $this->getOperation($cached_values); + $operations = $this->getOperations($cached_values); + $default_operation = reset($operations); + if ($operation['form'] == $default_operation['form']) { + // Create id and label form elements. + $form['name'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('fieldset-no-legend')), + '#title' => $this->getWizardLabel(), + ); + $form['name']['label'] = array( + '#type' => 'textfield', + '#title' => $this->getMachineLabel(), + '#required' => TRUE, + '#size' => 32, + '#default_value' => !empty($cached_values['label']) ? $cached_values['label'] : '', + '#maxlength' => 255, + '#disabled' => !empty($cached_values['label']), + ); + $form['name']['id'] = array( + '#type' => 'machine_name', + '#maxlength' => 128, + '#machine_name' => array( + 'source' => array('name', 'label'), + ), + '#description' => $this->t('A unique machine-readable name for this display. It must only contain lowercase letters, numbers, and underscores.'), + '#default_value' => !empty($cached_values['id']) ? $cached_values['id'] : '', + '#disabled' => !empty($cached_values['id']), + ); + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function getWizardLabel() { + return $this->t('Wizard Information'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Wizard name'); + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + $operations = [ + 'general' => [ + 'form' => PanelizerWizardGeneralForm::class, + 'title' => $this->t('General settings'), + ], + 'contexts' => [ + 'form' => PanelizerWizardContextForm::class, + 'title' => $this->t('Contexts'), + ], + ]; + + // Add any wizard operations from the plugin itself. + foreach ($cached_values['plugin']->getWizardOperations($cached_values) as $name => $operation) { + $operations[$name] = $operation; + } + + // Change the class that manages the Content step. + if (isset($operations['content'])) { + //$operations['content']['form'] = PanelizerWizardContentForm::class; + } + + return $operations; + } + + public function initValues() { + $cached_values = parent::initValues(); + $cached_values['access'] = new PanelizerUIAccess(); + if (empty($cached_values['plugin'])) { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = \Drupal::service('plugin.manager.display_variant')->createInstance('panels_variant'); + $plugin->setPattern('panelizer'); + $plugin->setBuilder('ipe'); + $plugin->setStorage('panelizer_default', 'TEMPORARY_STORAGE_ID'); + $cached_values['plugin'] = $plugin; + } + if (empty($cached_values['contexts'])) { + $cached_values['contexts'] = []; + } + return $cached_values; + } + + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + // Save the panels display mode and its custom settings as third party + // data of the display mode for this entity+bundle+display. + /** @var \Drupal\panelizer\Panelizer $panelizer */ + $panelizer = \Drupal::service('panelizer'); + /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */ + $invalidator = \Drupal::service('cache_tags.invalidator'); + list($entity_type, $bundle, $view_mode, $display_id) = explode('__', $cached_values['id']); + $panelizer->setDefaultPanelsDisplay($display_id, $entity_type, $bundle, $view_mode, $cached_values['plugin']); + $panelizer->setDisplayStaticContexts($display_id, $entity_type, $bundle, $view_mode, $cached_values['contexts']); + + parent::finish($form, $form_state); + $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id']]); + $invalidator->invalidateTags(["panelizer_default:$entity_type:$bundle:$view_mode:$display_id"]); + } + + /** + * Wraps the context mapper. + * + * @return \Drupal\ctools\ContextMapperInterface + */ + protected function getContextMapper() { + return \Drupal::service('ctools.context_mapper'); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + return $this->getContextMapper()->getContextValues($cached_values['contexts']); + } + +} diff --git a/templates/panelizer-wizard-form.html.twig b/templates/panelizer-wizard-form.html.twig new file mode 100644 index 0000000..a691162 --- /dev/null +++ b/templates/panelizer-wizard-form.html.twig @@ -0,0 +1,31 @@ +{# +/** + * @file + * Default theme implementation for a 'form' element. + * + * Available variables + * - attributes: A list of HTML attributes for the wrapper element. + * - children: The child elements of the form. + * + * @see template_preprocess_form() + * + * @ingroup themeable + */ +#} +
+
+ {{ form.wizard_actions }} +
+
+
+ {{ form.wizard_tree }} +
+
+ {{ form|without('wizard_actions', 'wizard_tree', 'actions') }} +
+
+ +
+ {{ form.actions }} +
+
diff --git a/templates/panelizer-wizard-tree.html.twig b/templates/panelizer-wizard-tree.html.twig new file mode 100644 index 0000000..7d48fcf --- /dev/null +++ b/templates/panelizer-wizard-tree.html.twig @@ -0,0 +1,47 @@ +{# +/** + * @file + * Default theme implementation to display wizard tree. + * + * Available variables: + * - step: The current step name. + * - tree: A nested list of menu items. Each menu item contains: + * - title: The menu link title. + * - url: The menu link url, instance of \Drupal\Core\Url + * - children: The menu item child items. + * - step: The name of the step. + * + * @ingroup themeable + */ +#} +{% import _self as panelizer %} + +{# + We call a macro which calls itself to render the full tree. + @see http://twig.sensiolabs.org/doc/tags/macro.html +#} +{{ panelizer.wizard_tree(tree, step, 0) }} + +{% macro wizard_tree(items, step, menu_level) %} + {% import _self as panelizer %} + {% if items %} + + {% endif %} +{% endmacro %}