commit f4e6824181708346912d8d1c49515ed7d279f070 Author: Nathan Coad Date: Wed Mar 4 15:06:41 2026 +1100 Initial standalone configurable views pane module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..777fd1b --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Configurable Views Pane + +Standalone Drupal module that provides one block plugin for Page Manager / Panels variants: + +- Plugin ID: `booking_configurable_views_pane` +- Form fields: `view_id`, `display_id`, `arguments` + +It lets editors set view target and contextual arguments per block instance, similar to legacy Drupal 7 pane behavior. + +## Install + +1. Place this module at: + `web/modules/custom/configurable_views_pane` +2. Enable module: + `drush en configurable_views_pane -y` +3. Rebuild cache: + `drush cr` + +## Use in Page Manager + +Add block **Configurable Views Pane** and set: + +- **View ID**: e.g. `audio_resource_embedded_player` +- **Display ID**: e.g. `default` or `block_1` +- **Contextual arguments**: e.g. `1770` (comma/pipe/newline separated) + +## Notes + +- This module intentionally keeps plugin id `booking_configurable_views_pane` for compatibility with any existing block config already using that id. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..90e4aaa --- /dev/null +++ b/composer.json @@ -0,0 +1,9 @@ +{ + "name": "custom/configurable_views_pane", + "description": "Drupal module providing configurable views pane block for Page Manager.", + "type": "drupal-module", + "license": "proprietary", + "require": { + "drupal/core": "^10 || ^11" + } +} diff --git a/configurable_views_pane.info.yml b/configurable_views_pane.info.yml new file mode 100644 index 0000000..b351b3a --- /dev/null +++ b/configurable_views_pane.info.yml @@ -0,0 +1,7 @@ +name: Configurable Views Pane +type: module +description: 'Reusable block plugin that lets editors configure view_id/display/arguments per Page Manager block instance.' +package: Custom +core_version_requirement: ^10 || ^11 +dependencies: + - drupal:views diff --git a/src/Plugin/Block/ConfigurableViewsPaneBlock.php b/src/Plugin/Block/ConfigurableViewsPaneBlock.php new file mode 100644 index 0000000..a4dd00a --- /dev/null +++ b/src/Plugin/Block/ConfigurableViewsPaneBlock.php @@ -0,0 +1,213 @@ + '', + 'display_id' => '', + 'arguments' => '', + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account): AccessResult { + $view_id = $this->normalizedViewId((string) ($this->configuration['view_id'] ?? '')); + $display_id = trim((string) ($this->configuration['display_id'] ?? '')); + if ($view_id === '' || $display_id === '') { + return AccessResult::forbidden(); + } + + $view = Views::getView($view_id); + if (!$view || !$this->displayExists($view_id, $display_id)) { + return AccessResult::forbidden(); + } + + $result = AccessResult::allowedIf($view->access($display_id, $account)); + if ($view->storage) { + $result = $result->addCacheableDependency($view->storage); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function build(): array { + $view_id = $this->normalizedViewId((string) ($this->configuration['view_id'] ?? '')); + $display_id = trim((string) ($this->configuration['display_id'] ?? '')); + if ($view_id === '' || $display_id === '') { + return []; + } + + $view = Views::getView($view_id); + if (!$view || !$this->displayExists($view_id, $display_id)) { + return []; + } + + $args = $this->parseArguments((string) ($this->configuration['arguments'] ?? '')); + $build = $view->buildRenderable($display_id, $args, FALSE); + if (!is_array($build)) { + return []; + } + + $build['#attributes']['class'][] = 'configurable-views-pane-block'; + return $build; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state): array { + $form = parent::blockForm($form, $form_state); + + $form['view_id'] = [ + '#type' => 'textfield', + '#title' => $this->t('View ID'), + '#default_value' => (string) ($this->configuration['view_id'] ?? ''), + '#required' => TRUE, + '#description' => $this->t('Machine name of the view. Example: audio_resource_embedded_player'), + ]; + $form['display_id'] = [ + '#type' => 'textfield', + '#title' => $this->t('Display ID'), + '#default_value' => (string) ($this->configuration['display_id'] ?? ''), + '#required' => TRUE, + '#description' => $this->t('Display machine name. Example: block_1, page_1, block_nid_1770'), + ]; + $form['arguments'] = [ + '#type' => 'textarea', + '#title' => $this->t('Contextual arguments'), + '#default_value' => (string) ($this->configuration['arguments'] ?? ''), + '#rows' => 2, + '#description' => $this->t('Optional contextual arguments. Separate multiple values with comma, pipe, or newline. Example: 1770'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockValidate($form, FormStateInterface $form_state): void { + parent::blockValidate($form, $form_state); + + $view_id = $this->normalizedViewId((string) $form_state->getValue('view_id')); + $display_id = trim((string) $form_state->getValue('display_id')); + + if ($view_id === '') { + $form_state->setErrorByName('view_id', $this->t('View ID is required.')); + return; + } + if ($display_id === '') { + $form_state->setErrorByName('display_id', $this->t('Display ID is required.')); + return; + } + + $view = Views::getView($view_id); + if (!$view) { + $form_state->setErrorByName('view_id', $this->t('View %view was not found.', ['%view' => $view_id])); + return; + } + + if (!$this->displayExists($view_id, $display_id)) { + $displays = implode(', ', $this->displayIds($view_id)); + $form_state->setErrorByName( + 'display_id', + $this->t('Display %display does not exist on view %view. Available displays: %available', [ + '%display' => $display_id, + '%view' => $view_id, + '%available' => $displays !== '' ? $displays : '', + ]) + ); + } + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state): void { + parent::blockSubmit($form, $form_state); + $this->configuration['view_id'] = $this->normalizedViewId((string) $form_state->getValue('view_id')); + $this->configuration['display_id'] = trim((string) $form_state->getValue('display_id')); + $this->configuration['arguments'] = trim((string) $form_state->getValue('arguments')); + } + + /** + * Checks whether a display exists on a view. + */ + private function displayExists(string $view_id, string $display_id): bool { + return in_array($display_id, $this->displayIds($view_id), TRUE); + } + + /** + * Returns known display IDs for a view. + * + * @return string[] + * Display IDs. + */ + private function displayIds(string $view_id): array { + $view = Views::getView($view_id); + if (!$view || !$view->storage) { + return []; + } + + $definitions = (array) ($view->storage->get('display') ?? []); + return array_values(array_filter(array_map(static function ($id) { + return is_scalar($id) ? trim((string) $id) : ''; + }, array_keys($definitions)), static function ($id) { + return $id !== ''; + })); + } + + /** + * Parses arguments from a config string. + * + * @return string[] + * Clean argument list. + */ + private function parseArguments(string $raw): array { + $raw = trim($raw); + if ($raw === '') { + return []; + } + + $parts = preg_split('/[\r\n,\|]+/', $raw) ?: []; + return array_values(array_filter(array_map(static function ($item) { + return trim((string) $item); + }, $parts), static function ($item) { + return $item !== ''; + })); + } + + /** + * Normalizes view machine names. + */ + private function normalizedViewId(string $value): string { + $value = strtolower(trim($value)); + return preg_replace('/[^a-z0-9_]+/', '_', $value) ?: ''; + } + +}