diff --git a/modules/ctools_inline_block/config/schema/ctools_inline_block.schema.yml b/modules/ctools_inline_block/config/schema/ctools_inline_block.schema.yml new file mode 100644 index 0000000..1541114 --- /dev/null +++ b/modules/ctools_inline_block/config/schema/ctools_inline_block.schema.yml @@ -0,0 +1,10 @@ +block.settings.inline_content:*: + type: block_settings + label: 'Inline block content' + mapping: + langcode: + type: string + label: 'Language code' + entity: + type: string + label: 'Serialized block_content entity' diff --git a/modules/ctools_inline_block/ctools_inline_block.info.yml b/modules/ctools_inline_block/ctools_inline_block.info.yml new file mode 100644 index 0000000..5765535 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.info.yml @@ -0,0 +1,9 @@ +name: 'Inline Blocks' +type: module +description: 'Exposes the custom block functionality of Drupal core as inline blocks what do not save to the database.' +package: 'Chaos tool suite' +version: 3.x +core: 8.x +dependencies: + - inline_entity_form + - block_content diff --git a/modules/ctools_inline_block/ctools_inline_block.install b/modules/ctools_inline_block/ctools_inline_block.install new file mode 100644 index 0000000..81c9110 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.install @@ -0,0 +1,30 @@ + [ + 'fields' => [ + 'uuid' => [ + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => 'UUID of the inline block.', + ], + 'loader' => [ + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'ID of the service which can load the block.', + ], + 'data' => [ + 'type' => 'text', + 'description' => 'Additional data needed to load the block.', + ], + ], + 'primary key' => ['uuid'], + ], + ]; +} diff --git a/modules/ctools_inline_block/ctools_inline_block.module b/modules/ctools_inline_block/ctools_inline_block.module new file mode 100644 index 0000000..b6b0265 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.module @@ -0,0 +1,143 @@ +getDefinition($form_display->getTargetEntityTypeId()) + ->get('mask'); + + if ($mask) { + $components = entity_get_form_display($mask, $form_display->getTargetBundle(), $form_display->getMode()) + ->getComponents(); + + foreach ($components as $id => $component) { + $form_display->setComponent($id, $component); + } + } +} + +/** + * Implements hook_entity_view_display(). + */ +function ctools_inline_block_entity_view_display_alter(EntityViewDisplayInterface $display, array $display_context) { + $mask = \Drupal::entityTypeManager() + ->getDefinition($display->getTargetEntityTypeId()) + ->get('mask'); + + if ($mask) { + $mask_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load("$mask.{$display->getTargetBundle()}.{$display->getMode()}"); + // If the masked display doesn't have a custom override load the default. + if (!$mask_display) { + $mask_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load("$mask.{$display->getTargetBundle()}.default"); + } + $components = $mask_display->getComponents(); + + foreach ($components as $id => $component) { + $display->setComponent($id, $component); + } + } +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function ctools_inline_block_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) { + $result = []; + + $mask = $entity_type->get('mask'); + if ($mask) { + $storages = ctools_inline_block_entity_field_storage_info($entity_type); + + /** @var \Drupal\field\FieldConfigInterface[] $fields */ + $fields = \Drupal::entityTypeManager() + ->getStorage('field_config') + ->loadByProperties([ + 'entity_type' => $mask, + 'bundle' => $bundle, + ]); + + foreach ($fields as $field) { + $field_name = $field->getName(); + + $result[$field_name] = $field->createDuplicate() + ->set('entity_type', $mask) + ->set('fieldStorage', $storages[$field_name]); + } + } + return $result; +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function ctools_inline_block_entity_field_storage_info(EntityTypeInterface $entity_type) { + $result = []; + + $mask = $entity_type->get('mask'); + if ($mask) { + /** @var \Drupal\field\FieldStorageConfigInterface[] $fields */ + $fields = \Drupal::entityTypeManager() + ->getStorage('field_storage_config') + ->loadByProperties([ + 'entity_type' => $mask, + ]); + + foreach ($fields as $field) { + $result[$field->getName()] = $field->createDuplicate() + ->set('entity_type', $mask); + } + } + return $result; +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + */ +function ctools_inline_block_block_insert(BlockInterface $block) { + $plugin = $block->getPlugin(); + + if ($plugin instanceof InlineBlock) { + \Drupal::database() + ->insert('inline_block') + ->fields([ + 'uuid' => $plugin->getEntity()->uuid(), + 'loader' => 'ctools_inline_block.block_loader', + 'data' => $block->id(), + ]) + ->execute(); + } +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function ctools_inline_block_block_delete(BlockInterface $block) { + $plugin = $block->getPlugin(); + + if ($plugin instanceof InlineBlock) { + $plugin->getEntity()->delete(); + } +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + */ +function ctools_inline_block_block_content_type_insert(BlockContentTypeInterface $blockContentType) { + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function ctools_inline_block_block_content_type_delete(BlockContentTypeInterface $blockContentType) { + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); +} diff --git a/modules/ctools_inline_block/ctools_inline_block.services.yml b/modules/ctools_inline_block/ctools_inline_block.services.yml new file mode 100644 index 0000000..f80cee5 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.services.yml @@ -0,0 +1,21 @@ +services: + ctools_inline_block.tempstore: + class: '\Drupal\ctools_inline_block\TempstoreBlockLoader' + arguments: + - '@user.shared_tempstore' + - '@plugin.manager.block' + ctools_inline_block.block_loader: + class: '\Drupal\ctools_inline_block\BlockLoader' + arguments: + - '@entity_type.manager' + ctools_inline_block.panels_block_loader: + class: '\Drupal\ctools_inline_block\PanelizerBlockLoader' + arguments: + - '@entity_type.manager' + - '@panelizer' + ctools_inline_block.event.inline_block: + class: '\Drupal\ctools_inline_block\EventSubscriber\InlineBlock' + arguments: + - '@database' + tags: + - { name: event_subscriber } diff --git a/modules/ctools_inline_block/src/BlockLoader.php b/modules/ctools_inline_block/src/BlockLoader.php new file mode 100644 index 0000000..40d413b --- /dev/null +++ b/modules/ctools_inline_block/src/BlockLoader.php @@ -0,0 +1,39 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function load($uuid, $data = NULL) { + // $data is expected to be the ID of the block config entity. + if ($data) { + return $this->entityTypeManager->getStorage('block')->load($data)->getPlugin()->getEntity(); + } + else { + throw new \InvalidArgumentException('No block entity ID was specified'); + } + } + +} diff --git a/modules/ctools_inline_block/src/BlockLoaderInterface.php b/modules/ctools_inline_block/src/BlockLoaderInterface.php new file mode 100644 index 0000000..526c064 --- /dev/null +++ b/modules/ctools_inline_block/src/BlockLoaderInterface.php @@ -0,0 +1,23 @@ +uuid(); + } + +} diff --git a/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php new file mode 100644 index 0000000..90c819c --- /dev/null +++ b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php @@ -0,0 +1,66 @@ +database + ->delete('inline_block') + ->condition('uuid', array_keys($entities), 'IN') + ->execute(); + } + + /** + * {@inheritdoc} + */ + protected function cleanIds(array $ids) { + return array_map('strval', $ids); + } + + /** + * {@inheritdoc} + */ + protected function getFromStorage(array $ids = NULL) { + $id_key = $this->idKey; + $this->idKey = 'uuid'; + $entities = parent::getFromStorage($ids); + $this->idKey = $id_key; + return $entities; + } + + /** + * {@inheritdoc} + */ + protected function buildQuery($ids, $revision_id = FALSE) { + $query = $this->database->select('inline_block', 'ib')->fields('ib'); + + if ($ids) { + $query->condition('uuid', $ids, 'IN'); + } + return $query; + } + + /** + * {@inheritdoc} + */ + protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) { + $mapped = []; + foreach ($records as $uuid => $record) { + // @TODO: Inject the service as a dependency. + $mapped[$uuid] = \Drupal::service($record->loader)->load($uuid, $record->data); + } + return $mapped; + } + +} diff --git a/modules/ctools_inline_block/src/EventSubscriber/InlineBlock.php b/modules/ctools_inline_block/src/EventSubscriber/InlineBlock.php new file mode 100644 index 0000000..577980f --- /dev/null +++ b/modules/ctools_inline_block/src/EventSubscriber/InlineBlock.php @@ -0,0 +1,61 @@ +connection = $connection; + } + + public static function getSubscribedEvents() { + $events[BlockVariantInterface::ADD_BLOCK][] = ['onVariantBlockAdd']; + $events[BlockVariantInterface::DELETE_BLOCK][] = ['onVariantBlockDelete']; + return $events; + } + + public function onVariantBlockAdd(BlockVariantEvent $event) { + $block = $event->getBlock(); + $block_plugin_id = $block->getPluginId(); + if ($block_plugin_id && explode(':', $block_plugin_id)[0] == 'inline_content') { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display_variant */ + $panels_display_variant = $event->getVariant(); + // Panelizer Default Display + $loader = 'ctools_inline_block.tempstore'; + //$data = "{$panels_display_variant->getStorageType()}:{$panels_display_variant->getStorageId()}"; + $data = $panels_display_variant->getTempStoreId(); + $this->connection + ->insert('inline_block') + ->fields([ + 'uuid' => $block->getConfiguration()['uuid'], + 'loader' => $loader, + 'data' => $data, + ]) + ->execute(); + } + } + + public function onVariantBlockDelete(BlockVariantEvent $event) { + $block = $event->getBlock(); + $block_plugin_id = $block->getPluginId(); + if ($block_plugin_id && explode(':', $block_plugin_id)[0] == 'inline_content') { + $this->connection + ->delete('inline_block') + ->condition('uuid', $block->getConfiguration()['uuid']) + ->execute(); + } + } + +} diff --git a/modules/ctools_inline_block/src/InlineStorage.php b/modules/ctools_inline_block/src/InlineStorage.php new file mode 100644 index 0000000..11f6114 --- /dev/null +++ b/modules/ctools_inline_block/src/InlineStorage.php @@ -0,0 +1,119 @@ +entityType = $entity_type; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + } + + /** + * {@inheritdoc} + */ + public function delete(array $entities) { + parent::delete($entities); + $this->resetCache(array_keys($entities)); + } + + /** + * {@inheritdoc} + */ + protected function doDeleteFieldItems($entities) { + } + + /** + * {@inheritdoc} + */ + protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) { + } + + /** + * {@inheritdoc} + */ + protected function deleteFromDedicatedTables(ContentEntityInterface $entity) { + } + + /** + * {@inheritdoc} + */ + protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) { + } + + /** + * {@inheritdoc} + */ + protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { + } + + /** + * {@inheritdoc} + */ + protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) { + } + + /** + * {@inheritdoc} + */ + protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) { + } + +} diff --git a/modules/ctools_inline_block/src/PanelsBlockLoader.php b/modules/ctools_inline_block/src/PanelsBlockLoader.php new file mode 100644 index 0000000..871574b --- /dev/null +++ b/modules/ctools_inline_block/src/PanelsBlockLoader.php @@ -0,0 +1,69 @@ +entityTypeManager = $entity_type_manager; + $this->panelizer = $panelizer; + } + + /** + * {@inheritdoc} + */ + public function load($uuid, $data = NULL) { + // $data is expected to be storage type and storage id. + if ($data) { + list($storage_type, $storage_id) = explode(':', $data, 2); + if ($storage_type == 'panelizer_default') { + list($entity_type, $bundle, $view_mode, $default) = explode(':', $storage_id); + $display = $this->panelizer->getDefaultPanelsDisplay($default, $entity_type, $bundle, $view_mode); + } + if ($storage_type == 'panelizer_field') { + list($entity_type, $entity_id, $view_mode, $revision_id) = explode(':', $storage_id); + /** @var FieldableEntityInterface $entity */ + if ($revision_id) { + $entity = $this->entityTypeManager->getStorage($entity_type)->loadRevision($revision_id); + } + else { + $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); + } + $display = $this->panelizer->getPanelsDisplay($entity, $view_mode); + } + if ($display) { + /** @var \Drupal\ctools_inline_block\Plugin\Block\InlineBlock $block */ + $block = $display->getBlock($uuid); + return $block->getEntity(); + } + } + throw new \InvalidArgumentException('No entity ID was specified'); + } + +} diff --git a/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php new file mode 100644 index 0000000..be3b6ba --- /dev/null +++ b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php @@ -0,0 +1,75 @@ +entity) && isset($this->configuration['entity'])) { + $this->entity = unserialize($this->configuration['entity']); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $options = $this->entityManager->getViewModeOptionsByBundle('block_content', $this->getDerivativeId()); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => $this->t('View mode'), + '#description' => $this->t('Output the block in this view mode.'), + '#default_value' => $this->configuration['view_mode'], + '#access' => (count($options) > 1), + ); + $form['title']['#description'] = $this->t('The title of the block as shown to the user.'); + $form['entity'] = [ + '#type' => 'inline_entity_form', + '#entity_type' => 'inline_block_content', + '#bundle' => $this->getDerivativeId(), + // If the #default_value is NULL, a new entity will be created. + '#default_value' => $this->getEntity(), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + /** @var \Drupal\block_content\BlockContentInterface $entity */ + $entity = !empty($form['settings']['entity']['#entity']) ? $form['settings']['entity']['#entity'] : $form['entity']['#entity']; + $this->configuration['label'] = $entity->label(); + $this->configuration['langcode'] = $form_state->getValue('langcode'); + + $this->entity = $entity; + $this->configuration['entity'] = serialize($entity); + } + +} diff --git a/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php new file mode 100644 index 0000000..7f91cd6 --- /dev/null +++ b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php @@ -0,0 +1,67 @@ +blockContentTypeStorage = $block_content_type_storage; + $this->stringTranslation = $translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity.manager')->getStorage('block_content_type'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $block_types = $this->blockContentTypeStorage->loadMultiple(); + // Reset the discovered definitions. + $this->derivatives = []; + /** @var $type \Drupal\block_content\Entity\BlockContent */ + foreach ($block_types as $id => $type) { + $this->derivatives[$id] = $base_plugin_definition; + $this->derivatives[$id]['admin_label'] = $this->t('Inline @type', ['@type' => $type->label()]); + $this->derivatives[$id]['config_dependencies']['content'] = array( + $type->getConfigDependencyName() + ); + } + return $this->derivatives; + } + +} diff --git a/modules/ctools_inline_block/src/TempstoreBlockLoader.php b/modules/ctools_inline_block/src/TempstoreBlockLoader.php new file mode 100644 index 0000000..ba1046a --- /dev/null +++ b/modules/ctools_inline_block/src/TempstoreBlockLoader.php @@ -0,0 +1,47 @@ +tempStore = $tempStore; + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function load($uuid, $data = NULL) { + $cached_values = $this->tempStore->get('panels_ipe')->get($data); + $block_configuration = $cached_values['blocks'][$uuid]; + if (!empty($block_configuration['entity'])) { + return unserialize($block_configuration['entity']); + } + } + +} diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml new file mode 100644 index 0000000..02982e4 --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml @@ -0,0 +1,5 @@ +id: basic +label: 'Basic block' +revision: 0 +description: 'A basic block contains a title and a body.' +langcode: en diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_form_display.block_content.basic.default.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_form_display.block_content.basic.default.yml new file mode 100644 index 0000000..5923ff8 --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_form_display.block_content.basic.default.yml @@ -0,0 +1,39 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.field.block_content.basic.body + - field.field.block_content.basic.field_puppies + - image.style.thumbnail + module: + - image + - text +id: block_content.basic.default +targetEntityType: block_content +bundle: basic +mode: default +content: + body: + type: text_textarea_with_summary + weight: 1 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } + field_puppies: + type: image_image + weight: 2 + settings: + progress_indicator: throbber + preview_image_style: thumbnail + third_party_settings: { } + info: + type: string_textfield + weight: 0 + settings: + size: 60 + placeholder: '' + third_party_settings: { } +hidden: { } diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml new file mode 100644 index 0000000..ad99a7e --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.field.block_content.basic.body + module: + - text +id: block_content.basic.default +targetEntityType: block_content +bundle: basic +mode: default +content: + body: + type: text_default + weight: 0 + label: hidden + settings: { } + third_party_settings: { } + field_puppies: + type: image + weight: 1 + label: above + settings: + image_style: '' + image_link: '' + third_party_settings: { } +hidden: { } diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml new file mode 100644 index 0000000..89118ef --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.storage.block_content.body + module: + - text +id: block_content.basic.body +field_name: body +entity_type: block_content +bundle: basic +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: false +field_type: text_with_summary diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.field_puppies.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.field_puppies.yml new file mode 100644 index 0000000..17168ee --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.field_puppies.yml @@ -0,0 +1,37 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.storage.block_content.field_puppies + module: + - image +id: block_content.basic.field_puppies +field_name: field_puppies +entity_type: block_content +bundle: basic +label: Puppies +description: 'This is where you should upload pictures of puppies.' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'png gif jpg jpeg' + max_filesize: '' + max_resolution: '' + min_resolution: '' + alt_field: false + alt_field_required: false + title_field: false + title_field_required: false + default_image: + uuid: '' + alt: '' + title: '' + width: null + height: null + handler: 'default:file' + handler_settings: { } +field_type: image diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.storage.block_content.field_puppies.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.storage.block_content.field_puppies.yml new file mode 100644 index 0000000..b5bee7e --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.storage.block_content.field_puppies.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - file + - image +id: block_content.field_puppies +field_name: field_puppies +entity_type: block_content +type: image +settings: + uri_scheme: public + default_image: + uuid: '' + alt: '' + title: '' + width: null + height: null + target_type: file + display_field: false + display_default: false +module: image +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml new file mode 100644 index 0000000..5c5b50b --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml @@ -0,0 +1,8 @@ +type: module +name: CTools Inline Block Test +description: 'Required for CTools Inline Block simpletests only.' +core: 8.x +dependencies: + - ctools_inline_block + - image + - text diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.module b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.module new file mode 100644 index 0000000..6f1a26a --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.module @@ -0,0 +1,11 @@ +uuid(); + } +} diff --git a/modules/ctools_inline_block/tests/src/Functional/RenderTest.php b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php new file mode 100644 index 0000000..020d381 --- /dev/null +++ b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php @@ -0,0 +1,68 @@ +assertSession(); + + $block_content = BlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(16), + 'body' => 'Yeah, I like animals better than people sometimes... Especially dogs.', + ]); + $block_content->save(); + + // Place the block using the normal block_content plugin and render + // it out. Nothing special here. + $block = $this->placeBlock('block_content:' . $block_content->uuid(), [ + 'region' => 'content', + ]); + $selector = '#block-' . $block->id(); + + $this->drupalGet('/user/login'); + + $concrete_output = $assert->elementExists('css', $selector)->getOuterHtml(); + + // Use the inline_content plugin to display the same block_content entity, + // serialized and stored in the plugin configuration. + $settings = $block->get('settings'); + $settings['entity'] = serialize($block_content); + $block + ->set('plugin', 'inline_content:basic') + ->set('settings', $settings) + ->save(); + + $this->getSession()->reload(); + $inline_output = $assert->elementExists('css', $selector)->getOuterHtml(); + + $this->assertSame($concrete_output, $inline_output); + } + +} diff --git a/modules/ctools_inline_block/tests/src/FunctionalJavascript/InlineBlockFieldTest.php b/modules/ctools_inline_block/tests/src/FunctionalJavascript/InlineBlockFieldTest.php new file mode 100644 index 0000000..0f46b19 --- /dev/null +++ b/modules/ctools_inline_block/tests/src/FunctionalJavascript/InlineBlockFieldTest.php @@ -0,0 +1,77 @@ +createUser(['administer blocks']); + $this->drupalLogin($account); + $this->drupalGet('/admin/structure/block/add/inline_content:basic/classy'); + + $page = $this->getSession()->getPage(); + + $image = \Drupal::moduleHandler()->getModule('image')->getPath() . '/sample.png'; + $page->attachFileToField('Puppies', \Drupal::root() . '/' . $image); + + $block_id = strtolower($this->randomMachineName(16)); + + // Wait for the file element to do its AJAX business. + $result = $this->getSession()->wait(10000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))'); + if (!$result) { + $this->fail('Timed out waiting for AJAX upload to complete.'); + } + $page->fillField('Block description', $this->randomMachineName()); + $page->fillField('id', $block_id); + $page->fillField('region', 'content'); + $page->pressButton('Save block'); + + $block = Block::load($block_id); + $block_content = $block->getPlugin()->getEntity(); + $this->assertInstanceOf(InlineBlockContent::class, $block_content); + $this->assertFalse($block_content->field_puppies->isEmpty()); + + /** @var \Drupal\file\FileInterface $image */ + $image = $block_content->field_puppies->entity; + /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ + $file_usage = \Drupal::service('file.usage'); + $this->assertTrue($image->isPermanent()); + $this->assertNotEmpty($file_usage->listUsage($image)); + + // Deleting the block should delete the inline block, which in turn should + // cause the image to be marked as temporary since nothing will be using it. + $block->delete(); + $this->assertEmpty($file_usage->listUsage($image)); + $image = \Drupal::entityTypeManager()->getStorage('file')->loadUnchanged($image->id()); + $this->assertTrue($image->isTemporary()); + } + +} diff --git a/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php new file mode 100644 index 0000000..2661ead --- /dev/null +++ b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php @@ -0,0 +1,278 @@ +installConfig('block_content'); + $this->installConfig('ctools_inline_block_test'); + + $this->installEntitySchema('block_content'); + $this->installEntitySchema('file'); + + $this->installSchema('ctools_inline_block', ['inline_block']); + } + + /** + * Tests that inline blocks go to /dev/null if saved on their own. + */ + public function testLoadWithoutBlock() { + $block_content = InlineBlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $block_content->save(); + + $id = $block_content->id(); + $this->assertNotEmpty($id); + $this->assertEquals($id, $block_content->uuid()); + + $this->assertNull(InlineBlockContent::load($id)); + $this->assertEmpty(InlineBlockContent::loadMultiple([$id])); + $this->assertEmpty(InlineBlockContent::loadMultiple()); + } + + /** + * Tests loading inline blocks by UUID. + * + * @depends testLoadWithoutBlock + */ + public function testLoadWithBlock() { + $block_content = InlineBlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $block_content->save(); + $id = $block_content->id(); + + $block = Block::create([ + 'id' => $this->randomMachineName(), + 'plugin' => 'inline_content:basic', + 'settings' => [ + 'entity' => serialize($block_content), + ], + ]); + $block->save(); + + $this->assertInstanceOf(InlineBlockContent::class, InlineBlockContent::load($id)); + + $loaded = InlineBlockContent::loadMultiple([$id]); + $this->assertInstanceOf(InlineBlockContent::class, $loaded[$id]); + } + + /** + * Tests that inline blocks can be created like normal custom blocks. + */ + public function testCreate() { + $body = 'Pasta ipsum dolor sit amet quadrefiore vermicelli rigatoncini fettuccine timpano. Mezzelune penne trofie penne lisce trenne trennette timpano corzetti tagliatelle calamaretti capunti fedelini corzetti farfalline.'; + + $block_content = InlineBlockContent::create([ + 'type' => 'basic', + 'body' => $body, + ]); + + // Assert that it's an inline block. + $this->assertInstanceOf(InlineBlockContent::class, $block_content); + $this->assertSame('inline_block_content', $block_content->getEntityTypeId()); + // Like any other entity, inline blocks should be new if unsaved. + $this->assertTrue($block_content->isNew()); + + // Assert that it has the same fields. + $this->assertTrue($block_content->hasField('body')); + $this->assertEquals($body, $block_content->body->value); + $this->assertTrue($block_content->hasField('field_puppies')); + $this->assertTrue($block_content->get('field_puppies')->isEmpty()); + } + + /** + * Tests that inline blocks don't put anything in the database when saved. + * + * @depends testCreate + */ + public function testSave() { + $block_content = InlineBlockContent::create([ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + 'body' => 'Creste di galli spaghettoni trennette pennette trennette penne zita linguettine orecchiette pappardelle gramigna casarecce.', + ]); + $block_content->save(); + + // Inline blocks use their UUID as their ID. + $this->assertEquals($block_content->id(), $block_content->uuid()); + // Like any other entity, inline blocks with an ID should not be new. + $this->assertFalse($block_content->isNew()); + + // Ensure that no concrete block_content entities were created. + $count = \Drupal::entityQuery('block_content')->count()->execute(); + $this->assertNumeric($count); + $this->assertEmpty($count); + + // Nothing should be written to the field tables. + $this->assertTableIsEmpty('block_content__body'); + $this->assertTableIsEmpty('block_content_revision__body'); + + // Inline blocks are invisible to entity queries. + $count = \Drupal::entityQuery('inline_block_content')->count()->execute(); + $this->assertNumeric($count); + $this->assertEmpty($count); + + // Nothing should be in the tracking table if saved directly. + $this->assertTableIsEmpty('inline_block'); + } + + /** + * Tests deleting an inline block directly. + * + * @depends testLoadWithBlock + */ + public function testDirectDelete() { + $block_content = InlineBlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $block_content->save(); + + $block = Block::create([ + 'id' => $this->randomMachineName(), + 'plugin' => 'inline_content:basic', + 'settings' => [ + 'entity' => serialize($block_content), + ], + ]); + $block->save(); + + // Because the inline block is associated with a real block, there should + // be an entry in the tracking table. + $this->assertTableIsNotEmpty('inline_block'); + + // Deleting the inline block should clear it from the tracking table... + $block_content->delete(); + $this->assertTableIsEmpty('inline_block'); + + // ...which means it should no longer be loadable. + $id = $block_content->id(); + $this->assertNull(InlineBlockContent::load($id)); + } + + /** + * Tests deleting a block that contains an inline block. + * + * @depends testDirectDelete + */ + public function testDeleteBlock() { + $block_content = InlineBlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $block_content->save(); + + $block = Block::create([ + 'id' => $this->randomMachineName(), + 'plugin' => 'inline_content:basic', + 'settings' => [ + 'entity' => serialize($block_content), + ], + ]); + $block->save(); + + // Because the inline block is associated with a real block, there should + // be an entry in the tracking table. + $this->assertTableIsNotEmpty('inline_block'); + + // Deleting the block should clear the inline block from the tracking table. + $block->delete(); + $this->assertTableIsEmpty('inline_block'); + + // ...which means it should no longer be loadable. + $id = $block_content->id(); + $this->assertNull(InlineBlockContent::load($id)); + } + + /** + * Asserts that a value is numeric. + * + * @param mixed $value + * The value to check. + */ + protected function assertNumeric($value) { + $this->assertTrue(is_numeric($value)); + } + + /** + * Counts the rows in a database table. + * + * @param string $table + * The table to count. + * + * @return int + * The number of rows in the table. + */ + protected function countTable($table) { + $count = \Drupal::database() + ->select($table) + ->countQuery() + ->execute() + ->fetchField(); + + $this->assertNumeric($count); + return (int) $count; + } + + /** + * Asserts that a database table is empty. + * + * @param string $table + * The table to check. + */ + protected function assertTableIsEmpty($table) { + $this->assertEmpty($this->countTable($table)); + } + + /** + * Asserts that a database table is not empty. + * + * @param string $table + * The table to check. + */ + protected function assertTableIsNotEmpty($table) { + $this->assertNotEmpty($this->countTable($table)); + } + +} diff --git a/src/Event/BlockVariantEvent.php b/src/Event/BlockVariantEvent.php new file mode 100644 index 0000000..9aa7f05 --- /dev/null +++ b/src/Event/BlockVariantEvent.php @@ -0,0 +1,58 @@ +block = $block; + $this->variant = $variant; + } + + /** + * Gets the block plugin. + * + * @return BlockPluginInterface + */ + public function getBlock() { + return $this->block; + } + + /** + * Gets the variant plugin. + * + * @return BlockVariantInterface + */ + public function getVariant() { + return $this->variant; + } + +} diff --git a/src/Plugin/BlockVariantInterface.php b/src/Plugin/BlockVariantInterface.php index aa2709a..4aec7cd 100644 --- a/src/Plugin/BlockVariantInterface.php +++ b/src/Plugin/BlockVariantInterface.php @@ -10,6 +10,21 @@ use Drupal\Core\Display\VariantInterface; interface BlockVariantInterface extends VariantInterface { /** + * Denotes that a block is being added to the variant. + */ + CONST ADD_BLOCK = 'add.block'; + + /** + * Denotes that a block is being updated in the variant. + */ + CONST UPDATE_BLOCK = 'update.block'; + + /** + * Denotes that a block is being deleted from the variant. + */ + CONST DELETE_BLOCK = 'delete.block'; + + /** * Returns the human-readable list of regions keyed by machine name. * * @return array diff --git a/src/Plugin/BlockVariantTrait.php b/src/Plugin/BlockVariantTrait.php index fa01870..45a8ad7 100644 --- a/src/Plugin/BlockVariantTrait.php +++ b/src/Plugin/BlockVariantTrait.php @@ -2,6 +2,8 @@ namespace Drupal\ctools\Plugin; +use Drupal\ctools\Event\BlockVariantEvent; + /** * Provides methods for \Drupal\ctools\Plugin\BlockVariantInterface. */ @@ -22,6 +24,13 @@ trait BlockVariantTrait { protected $blockPluginCollection; /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcher + */ + protected $dispatcher; + + /** * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionNames() */ abstract public function getRegionNames(); @@ -39,6 +48,11 @@ trait BlockVariantTrait { public function addBlock(array $configuration) { $configuration['uuid'] = $this->uuidGenerator()->generate(); $this->getBlockCollection()->addInstanceId($configuration['uuid'], $configuration); + + $block = $this->getBlock($configuration['uuid']); + $event = new BlockVariantEvent($block, $this); + $this->getDispatcher()->dispatch(BlockVariantInterface::ADD_BLOCK, $event); + return $configuration['uuid']; } @@ -46,6 +60,9 @@ trait BlockVariantTrait { * @see \Drupal\ctools\Plugin\BlockVariantInterface::removeBlock() */ public function removeBlock($block_id) { + $block = $this->getBlock($block_id); + $event = new BlockVariantEvent($block, $this); + $this->getDispatcher()->dispatch(BlockVariantInterface::DELETE_BLOCK, $event); $this->getBlockCollection()->removeInstanceId($block_id); return $this; } @@ -54,8 +71,11 @@ trait BlockVariantTrait { * @see \Drupal\ctools\Plugin\BlockVariantInterface::updateBlock() */ public function updateBlock($block_id, array $configuration) { - $existing_configuration = $this->getBlock($block_id)->getConfiguration(); + $block = $this->getBlock($block_id); + $existing_configuration = $block->getConfiguration(); $this->getBlockCollection()->setInstanceConfiguration($block_id, $configuration + $existing_configuration); + $event = new BlockVariantEvent($block, $this); + $this->getDispatcher()->dispatch(BlockVariantInterface::UPDATE_BLOCK, $event); return $this; } @@ -113,6 +133,18 @@ trait BlockVariantTrait { } /** + * Gets the event dispatcher. + * + * @return \Symfony\Component\EventDispatcher\EventDispatcher + */ + protected function getDispatcher() { + if (!$this->dispatcher) { + $this->dispatcher = \Drupal::service('event_dispatcher'); + } + return $this->dispatcher; + } + + /** * Returns the UUID generator. * * @return \Drupal\Component\Uuid\UuidInterface diff --git a/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/src/Plugin/DisplayVariant/BlockDisplayVariant.php index 32c6e86..2632b2d 100644 --- a/src/Plugin/DisplayVariant/BlockDisplayVariant.php +++ b/src/Plugin/DisplayVariant/BlockDisplayVariant.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a base class for a display variant that simply contains blocks. */ -abstract class BlockDisplayVariant extends VariantBase implements ContextAwareVariantInterface, ContainerFactoryPluginInterface, BlockVariantInterface, RefinableCacheableDependencyInterface { +abstract class BlockDisplayVariant extends VariantBase implements ContextAwareVariantInterface, ContainerFactoryPluginInterface, BlockVariantInterface { use AjaxFormTrait; use BlockVariantTrait;