diff --git a/config/schema/page_manager.schema.yml b/config/schema/page_manager.schema.yml index add031c..335121a 100644 --- a/config/schema/page_manager.schema.yml +++ b/config/schema/page_manager.schema.yml @@ -73,6 +73,12 @@ page_manager.page_variant.*: value: type: string label: 'Context value' + relationships: + type: sequence + label: 'Relationships' + sequence: + - type: ctools.relationship + label: 'Relationship' page_manager.block_plugin.*: type: block.settings.[id] diff --git a/page_manager.routing.yml b/page_manager.routing.yml index 7ba4827..fea4019 100644 --- a/page_manager.routing.yml +++ b/page_manager.routing.yml @@ -108,6 +108,32 @@ page_manager.static_context_delete: requirements: _entity_access: page_variant.update +#### Relationships + +page_manager.relationship_add: + path: '/admin/structure/page_manager/manage/{page}/variant/{page_variant}/relationship/add' + defaults: + _form: '\Drupal\page_manager\Form\RelationshipAddForm' + _title: 'Add new relationship' + requirements: + _entity_access: page_variant.update + +page_manager.relationship_edit: + path: '/admin/structure/page_manager/manage/{page}/variant/{page_variant}/relationship/edit/{name}' + defaults: + _form: '\Drupal\page_manager\Form\RelationshipEditForm' + _title_callback: '\Drupal\page_manager\Controller\PageManagerController::editRelationshipTitle' + requirements: + _entity_access: page_variant.update + +page_manager.relationship_delete: + path: '/admin/structure/page_manager/manage/{page}/variant/{page_variant}/relationship/delete/{name}' + defaults: + _form: '\Drupal\page_manager\Form\RelationshipDeleteForm' + _title: 'Delete relationship' + requirements: + _entity_access: page_variant.update + #### Variants page_manager.variant_select: diff --git a/page_manager.services.yml b/page_manager.services.yml index 4d7db13..711fb89 100644 --- a/page_manager.services.yml +++ b/page_manager.services.yml @@ -11,7 +11,7 @@ services: - { name: 'event_subscriber' } page_manager.context_mapper: class: Drupal\page_manager\ContextMapper - arguments: ['@entity.repository'] + arguments: ['@entity.repository', '@ctools.typed_data.resolver'] page_manager.page_manager_routes: class: Drupal\page_manager\Routing\PageManagerRoutes arguments: ['@entity_type.manager', '@cache_tags.invalidator'] diff --git a/src/ContextMapper.php b/src/ContextMapper.php index 101bf2f..c4c04cc 100644 --- a/src/ContextMapper.php +++ b/src/ContextMapper.php @@ -10,6 +10,7 @@ namespace Drupal\page_manager; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\ctools\TypedDataResolver; use Drupal\page_manager\Context\EntityLazyLoadContext; /** @@ -25,13 +26,21 @@ class ContextMapper implements ContextMapperInterface { protected $entityRepository; /** + * The typed data resolver. + * + * @var \Drupal\ctools\TypedDataResolver + */ + protected $typedDataResolver; + + /** * Constructs a new ContextMapper. * * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. */ - public function __construct(EntityRepositoryInterface $entity_repository) { + public function __construct(EntityRepositoryInterface $entity_repository, TypedDataResolver $typed_data_resolver) { $this->entityRepository = $entity_repository; + $this->typedDataResolver = $typed_data_resolver; } /** @@ -52,4 +61,17 @@ class ContextMapper implements ContextMapperInterface { return $contexts; } + /** + * {@inheritdoc} + */ + public function getRelationshipValues(array $relationship_configurations, array $contexts) { + $relationships = []; + foreach ($relationship_configurations as $token => $relationship_configuration) { + $context = $this->typedDataResolver->convertTokenToContext($token, $contexts); + $context->getContextDefinition()->setLabel($relationship_configuration['label']); + $relationships[$relationship_configuration['id']] = $context; + } + return $relationships; + } + } diff --git a/src/ContextMapperInterface.php b/src/ContextMapperInterface.php index 9f76cc1..765214b 100644 --- a/src/ContextMapperInterface.php +++ b/src/ContextMapperInterface.php @@ -23,4 +23,17 @@ interface ContextMapperInterface { */ public function getContextValues(array $static_context_configurations); + /** + * Gathers the relationship values. + * + * @param array[] $relationship_configurations + * An array of relationship configurations. + * @param \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of contexts. + * + * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of set context values, keyed by context name. + */ + public function getRelationshipValues(array $relationship_configurations, array $contexts); + } diff --git a/src/Controller/PageManagerController.php b/src/Controller/PageManagerController.php index 7c48bc3..44867c1 100644 --- a/src/Controller/PageManagerController.php +++ b/src/Controller/PageManagerController.php @@ -159,6 +159,22 @@ class PageManagerController extends ControllerBase { } /** + * Route title callback. + * + * @param \Drupal\page_manager\PageVariantInterface $page_variant + * The page variant entity. + * @param string $name + * The relationship token. + * + * @return string + * The title for the relationship edit form. + */ + public function editRelationshipTitle(PageVariantInterface $page_variant, $name) { + $relationship = $page_variant->getRelationship($name); + return $this->t('Edit @label relationship', ['@label' => $relationship['label']]); + } + + /** * Enables or disables a Page. * * @param \Drupal\page_manager\PageInterface $page diff --git a/src/Entity/PageVariant.php b/src/Entity/PageVariant.php index c1198c8..3b1aedf 100644 --- a/src/Entity/PageVariant.php +++ b/src/Entity/PageVariant.php @@ -47,6 +47,7 @@ use Drupal\page_manager\PageVariantInterface; * "selection_criteria", * "selection_logic", * "static_context", + * "relationships", * }, * links = { * "edit-form" = "/admin/structure/page_manager/manage/{page}/variant/{page_variant}", @@ -139,6 +140,15 @@ class PageVariant extends ConfigEntityBase implements PageVariantInterface { protected $static_context = []; /** + * Relationship references. + * + * A list of arrays with the keys id, and label. + * + * @var array[] + */ + protected $relationships = []; + + /** * The plugin collection that holds the single variant plugin instance. * * @var \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection @@ -257,7 +267,9 @@ class PageVariant extends ConfigEntityBase implements PageVariantInterface { if (is_null($this->contexts)) { $static_contexts = $this->getContextMapper()->getContextValues($this->getStaticContexts()); $page_contexts = $this->getPage()->getContexts(); - $this->contexts = array_merge($static_contexts, $page_contexts); + $contexts = array_merge($static_contexts, $page_contexts); + $relationships = $this->getContextMapper()->getRelationshipValues($this->getRelationships(), $contexts); + $this->contexts = array_merge($contexts, $relationships); } return $this->contexts; } @@ -373,6 +385,39 @@ class PageVariant extends ConfigEntityBase implements PageVariantInterface { /** * {@inheritdoc} */ + public function getRelationships() { + return $this->relationships; + } + + /** + * {@inheritdoc} + */ + public function getRelationship($token) { + if (isset($this->relationships[$token])) { + return $this->relationships[$token]; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setRelationship($token, $configuration) { + $this->relationships[$token] = $configuration; + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeRelationship($token) { + unset($this->relationships[$token]); + return $this; + } + + /** + * {@inheritdoc} + */ protected function urlRouteParameters($rel) { $parameters = parent::urlRouteParameters($rel); $parameters['page'] = $this->get('page'); diff --git a/src/Form/PageVariantEditForm.php b/src/Form/PageVariantEditForm.php index 76cc2cc..9cb711f 100644 --- a/src/Form/PageVariantEditForm.php +++ b/src/Form/PageVariantEditForm.php @@ -40,6 +40,8 @@ class PageVariantEditForm extends PageVariantFormBase { $form['context'] = $this->buildContextForm(); + $form['relationships'] = $this->buildRelationshipForm(); + return $form; } @@ -305,6 +307,12 @@ class PageVariantEditForm extends PageVariantFormBase { $attributes = $this->getAjaxAttributes(); $add_button_attributes = $this->getAjaxButtonAttributes(); + // Make a lookup table of relationships to easily find them by id. + $relationships = []; + foreach ($page_variant->getRelationships() as $token => $relationship) { + $relationships[$relationship['id']] = $relationship; + } + $form = [ '#type' => 'details', '#title' => $this->t('Available context'), @@ -336,6 +344,11 @@ class PageVariantEditForm extends PageVariantFormBase { ]; $contexts = $page_variant->getContexts(); foreach ($contexts as $name => $context) { + // Skip relationships - they get their own section. + if ($relationships[$name]) { + continue; + } + $context_definition = $context->getContextDefinition(); $row = []; @@ -383,6 +396,91 @@ class PageVariantEditForm extends PageVariantFormBase { } /** + * Builds the relationship form for a variant. + * + * @return array + */ + protected function buildRelationshipForm() { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $this->getEntity(); + + // Set up the attributes used by a modal to prevent duplication later. + $attributes = $this->getAjaxAttributes(); + $add_button_attributes = $this->getAjaxButtonAttributes(); + + $form = [ + '#type' => 'details', + '#title' => $this->t('Relationships'), + '#open' => TRUE, + ]; + $form['add'] = [ + '#type' => 'link', + '#title' => $this->t('Add new relationship'), + '#url' => Url::fromRoute('page_manager.relationship_add', [ + 'page' => $page_variant->get('page'), + 'page_variant' => $page_variant->id(), + ]), + '#attributes' => $add_button_attributes, + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + ], + ], + ]; + $form['available_context'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Label'), + $this->t('Name'), + $this->t('Token'), + $this->t('Operations'), + ], + '#empty' => $this->t('There is no available context.'), + ]; + foreach ($page_variant->getRelationships() as $token => $relationship) { + $row = []; + $row['label'] = [ + '#markup' => $relationship['label'], + ]; + $row['machine_name'] = [ + '#markup' => $relationship['id'], + ]; + $row['token'] = [ + '#markup' => $token, + ]; + + // Add operation links if the context is a static context. + $operations = []; + $operations['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('page_manager.relationship_edit', [ + 'page' => $page_variant->get('page'), + 'page_variant' => $page_variant->id(), + 'name' => $token, + ]), + 'attributes' => $attributes, + ]; + $operations['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('page_manager.relationship_delete', [ + 'page' => $page_variant->get('page'), + 'page_variant' => $page_variant->id(), + 'name' => $token, + ]), + 'attributes' => $attributes, + ]; + $row['operations'] = [ + '#type' => 'operations', + '#links' => $operations, + ]; + + $form['available_context'][$name] = $row; + } + + return $form; + } + + /** * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { @@ -397,6 +495,9 @@ class PageVariantEditForm extends PageVariantFormBase { } } + // Unset 'relationships' because it doesn't hold the complete data. + $form_state->unsetValue('relationships'); + parent::save($form, $form_state); $form_state->setRedirect('entity.page.edit_form', ['page' => $this->entity->get('page')]); diff --git a/src/Form/RelationshipAddForm.php b/src/Form/RelationshipAddForm.php new file mode 100644 index 0000000..b55bc7d --- /dev/null +++ b/src/Form/RelationshipAddForm.php @@ -0,0 +1,36 @@ +t('Add relationship'); + } + + /** + * {@inheritdoc} + */ + protected function submitMessageText() { + return $this->t('The %label relationship has been added.', ['%label' => $this->relationship['label']]); + } + +} diff --git a/src/Form/RelationshipDeleteForm.php b/src/Form/RelationshipDeleteForm.php new file mode 100644 index 0000000..4bcaac1 --- /dev/null +++ b/src/Form/RelationshipDeleteForm.php @@ -0,0 +1,80 @@ +t('Are you sure you want to delete the relationship %label?', ['%label' => $this->pageVariant->getRelationship($this->relationship)['label']]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->pageVariant->toUrl('edit-form'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, PageVariantInterface $page_variant = NULL, $name = NULL) { + $this->pageVariant = $page_variant; + $this->relationship = $name; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message($this->t('The relationship %label has been removed.', ['%label' => $this->pageVariant->getRelationship($this->relationship)['label']])); + $this->pageVariant->removeRelationship($this->relationship); + $this->pageVariant->save(); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/src/Form/RelationshipEditForm.php b/src/Form/RelationshipEditForm.php new file mode 100644 index 0000000..0644887 --- /dev/null +++ b/src/Form/RelationshipEditForm.php @@ -0,0 +1,52 @@ +t('Update relationship'); + } + + /** + * {@inheritdoc} + */ + protected function submitMessageText() { + return $this->t('The %label relationship has been updated.', ['%label' => $this->relationship['label']]); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, PageVariantInterface $page_variant = NULL, $name = '') { + $form = parent::buildForm($form, $form_state, $page_variant, $name); + // The token of an existing relationship is read-only. + $form['token'] = array( + '#type' => 'value', + '#value' => $name, + ); + return $form; + } + +} diff --git a/src/Form/RelationshipFormBase.php b/src/Form/RelationshipFormBase.php new file mode 100644 index 0000000..c1a9052 --- /dev/null +++ b/src/Form/RelationshipFormBase.php @@ -0,0 +1,147 @@ +typedDataResolver = $typed_data_resolver; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('ctools.typed_data.resolver') + ); + } + + /** + * Returns the text to use for the submit button. + * + * @return string + * The submit button text. + */ + abstract protected function submitButtonText(); + + /** + * Returns the text to use for the submit message. + * + * @return string + * The submit message text. + */ + abstract protected function submitMessageText(); + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, PageVariantInterface $page_variant = NULL, $name = '') { + $this->pageVariant = $page_variant; + $this->relationship = $this->pageVariant->getRelationship($name); + + $form['token'] = [ + '#type' => 'select', + '#title' => $this->t('Relationship'), + '#options' => $this->typedDataResolver->getTokensForContexts($this->pageVariant->getContexts()), + '#default_value' => $name, + ]; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#default_value' => isset($this->relationship['label']) ? $this->relationship['label'] : '', + '#required' => TRUE, + ]; + $form['id'] = [ + '#type' => 'machine_name', + '#maxlength' => 64, + '#required' => TRUE, + '#machine_name' => [ + 'exists' => [$this, 'contextExists'], + 'source' => ['label'], + ], + '#default_value' => isset($this->relationship['id']) ? $this->relationship['id'] : '', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->submitButtonText(), + '#button_type' => 'primary', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->relationship = [ + 'id' => $form_state->getValue('id'), + 'label' => $form_state->getValue('label'), + ]; + $this->pageVariant->setRelationship($form_state->getValue('token'), $this->relationship); + $this->pageVariant->save(); + + // Set the submission message. + drupal_set_message($this->submitMessageText()); + + $form_state->setRedirectUrl($this->pageVariant->toUrl('edit-form')); + } + + /** + * Determines if a context with that name already exists. + * + * @param string $name + * The context name + * + * @return bool + * TRUE if the format exists, FALSE otherwise. + */ + public function contextExists($name) { + return isset($this->pageVariant->getContexts()[$name]); + } + +} diff --git a/src/PageVariantInterface.php b/src/PageVariantInterface.php index 12110c7..39e1b1c 100644 --- a/src/PageVariantInterface.php +++ b/src/PageVariantInterface.php @@ -148,4 +148,45 @@ interface PageVariantInterface extends ConfigEntityInterface, EntityWithPluginCo */ public function removeStaticContext($name); + /** + * Returns the relationships for this variant entity. + * + * @return array[] + * An array of relationship configurations keyed by token name. + */ + public function getRelationships(); + + /** + * Retrieves a specific relationship. + * + * @param string $token + * The relationship token. + * + * @return array + * The configuration array of the relationship. + */ + public function getRelationship($token); + + /** + * Adds/updates a given relationship. + * + * @param string $token + * The relationship token. + * @param array $configuration + * A new array of configuration for the relationship. + * + * @return $this + */ + public function setRelationship($token, $configuration); + + /** + * Removes a specific relationship. + * + * @param string $token + * The relationship token. + * + * @return $this + */ + public function removeRelationship($token); + }