Browse Source

Integer-based weight drag-n-drop (Issue 1262) (#171)

* add integer_weight_selector

* test integer weight drag-n-drop

* move reorder_children view to core
pull/729/head
Seth Shaw 5 years ago committed by dannylamb
parent
commit
d45f948017
  1. 6
      islandora.links.action.yml
  2. 44
      islandora.module
  3. 25
      islandora.views.inc
  4. 271
      modules/islandora_core_feature/config/install/views.view.reorder_children.yml
  5. 4
      src/EventSubscriber/AdminViewsRouteSubscriber.php
  6. 101
      src/Plugin/views/field/IntegerWeightSelector.php
  7. 9
      tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml
  8. 251
      tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml
  9. 201
      tests/src/FunctionalJavascript/IntegerWeightTest.php

6
islandora.links.action.yml

@ -9,3 +9,9 @@ islandora.add_member_to_node:
title: Add child title: Add child
appears_on: appears_on:
- view.manage_members.page_1 - view.manage_members.page_1
islandora.reorder_children:
route_name: view.reorder_children.page_1
title: Reorder Children
appears_on:
- view.manage_members.page_1

44
islandora.module

@ -409,3 +409,47 @@ function islandora_entity_view(array &$build, EntityInterface $entity, EntityVie
} }
} }
} }
/**
* Implements hook_preprocess_views_view_table().
*
* Used for the integer-weight drag-n-drop. Taken almost
* verbatim from the weight module.
*/
function islandora_preprocess_views_view_table(&$variables) {
// Check for a weight selector field.
foreach ($variables['view']->field as $field_key => $field) {
if ($field->options['plugin_id'] == 'integer_weight_selector') {
// Check if the weight selector is on the first column.
$is_first_column = array_search($field_key, array_keys($variables['view']->field)) > 0 ? FALSE : TRUE;
// Add the tabledrag attributes.
foreach ($variables['rows'] as $key => $row) {
if ($is_first_column) {
// If the weight selector is the first column move it to the last
// column, in order to make the draggable widget appear.
$weight_selector = $variables['rows'][$key]['columns'][$field->field];
unset($variables['rows'][$key]['columns'][$field->field]);
$variables['rows'][$key]['columns'][$field->field] = $weight_selector;
}
// Add draggable attribute.
$variables['rows'][$key]['attributes']->addClass('draggable');
}
// The row key identify in an unique way a view grouped by a field.
// Without row number, all the groups will share the same table_id
// and just the first table can be draggable.
$table_id = 'weight-table-' . $variables['view']->dom_id . '-row-' . $key;
$variables['attributes']['id'] = $table_id;
$options = [
'table_id' => $table_id,
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight-selector',
];
drupal_attach_tabledrag($variables, $options);
}
}
}

25
islandora.views.inc

@ -0,0 +1,25 @@
<?php
/**
* @file
* Provide Views data for integer-weight fields.
*/
/**
* Implements hook_views_data_alter().
*/
function islandora_views_data_alter(&$data) {
// For now only support Nodes.
$fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node');
foreach ($fields as $field => $field_storage_definition) {
if ($field_storage_definition->getType() == 'integer' && strpos($field, "field_") === 0) {
$data['node__' . $field][$field . '_value']['field'] = $data['node__' . $field][$field]['field'];
$data['node__' . $field][$field]['title'] = t('Integer Weight Selector (@field)', [
'@field' => $field,
]);
$data['node__' . $field][$field]['help'] = t('Provides a drag-n-drop reordering of integer-based weight fields.');
$data['node__' . $field][$field]['title short'] = t('Integer weight selector');
$data['node__' . $field][$field]['field']['id'] = 'integer_weight_selector';
}
}
}

271
modules/islandora_core_feature/config/install/views.view.reorder_children.yml

@ -0,0 +1,271 @@
langcode: en
status: true
dependencies:
enforced:
module:
- islandora_core_feature
module:
- islandora
- node
- user
id: reorder_children
label: 'Reorder children'
module: views
description: 'Manage members belonging to a piece of content'
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'manage members'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
tags:
previous: ‹‹
next: ››
first: '« First'
last: 'Last »'
expose:
items_per_page: true
items_per_page_label: 'Items per page'
items_per_page_options: '10, 25, 50, 100'
items_per_page_options_all: true
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
style:
type: table
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
field_weight:
id: field_weight
table: node__field_weight
field: field_weight
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: integer_weight_selector
filters: { }
sorts:
field_weight_value:
id: field_weight_value
table: node__field_weight
field: field_weight_value
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
plugin_id: standard
title: 'Reorder children'
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
field_member_of_target_id:
id: field_member_of_target_id
table: node__field_member_of
field: field_member_of_target_id
relationship: none
group_type: group
admin_label: ''
default_action: default
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: node
default_argument_options: { }
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: true
validate:
type: 'entity:node'
fail: 'not found'
validate_options:
operation: view
multiple: 0
bundles: { }
access: false
break_phrase: false
not: false
plugin_id: numeric
display_extenders: { }
filter_groups:
operator: AND
groups: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: node/%node/reorder
menu:
type: tab
title: 'Reorder Children'
description: ''
expanded: false
parent: ''
weight: 0
context: '0'
menu_name: main
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

4
src/EventSubscriber/AdminViewsRouteSubscriber.php

@ -24,6 +24,10 @@ class AdminViewsRouteSubscriber extends RouteSubscriberBase {
$route->setRequirement('_permission', 'manage members'); $route->setRequirement('_permission', 'manage members');
$route->setRequirement('_custom_access', '\Drupal\islandora\Controller\ManageMediaController::access'); $route->setRequirement('_custom_access', '\Drupal\islandora\Controller\ManageMediaController::access');
} }
if ($route = $collection->get('view.reorder_children.page_1')) {
$route->setOption('_admin_route', 'TRUE');
$route->setRequirement('_permission', 'manage members');
}
} }
} }

101
src/Plugin/views/field/IntegerWeightSelector.php

@ -0,0 +1,101 @@
<?php
namespace Drupal\islandora\Plugin\views\field;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ResultRow;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
/**
* Field handler to present a weight selector element.
*
* A port of the weight module's weight selector element
* to support an unsigned integer using the values found
* in the result set.
*
* @ingroup views_field_handlers
*
* @ViewsField("integer_weight_selector")
*/
class IntegerWeightSelector extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
return ViewsRenderPipelineMarkup::create('<!--form-item-' . $this->options['id'] . '--' . $this->view->row_index . '-->');
}
/**
* {@inheritdoc}
*/
public function viewsForm(array &$form, FormStateInterface $form_state) {
// The view is empty, abort.
if (empty($this->view->result)) {
return;
}
$form[$this->options['id']] = [
'#tree' => TRUE,
];
// Use the existing values of this result set to populate the options.
$options = [];
foreach ($this->view->result as $row_index => $row) {
$options[$this->getValue($row)] = $this->getValue($row);
}
// Now that we have all the available weight values, populate the forms.
foreach ($this->view->result as $row_index => $row) {
$entity = $row->_entity;
$field_langcode = $entity->getEntityTypeId() . '__' . $this->field . '_langcode';
$form[$this->options['id']][$row_index]['weight'] = [
'#type' => 'select',
'#options' => $options,
'#default_value' => $this->getValue($row),
'#attributes' => ['class' => ['weight-selector']],
];
$form[$this->options['id']][$row_index]['entity'] = [
'#type' => 'value',
'#value' => $entity,
];
$form[$this->options['id']][$row_index]['langcode'] = [
'#type' => 'value',
'#value' => $row->{$field_langcode},
];
}
$form['views_field'] = [
'#type' => 'value',
'#value' => $this->field,
];
$form['#action'] = \Drupal::request()->getRequestUri();
}
/**
* {@inheritdoc}
*/
public function viewsFormSubmit(array &$form, FormStateInterface $form_state) {
$field_name = $form_state->getValue('views_field');
$rows = $form_state->getValue($field_name);
foreach ($rows as $row) {
if ($row['langcode']) {
$entity = $row['entity']->getTranslation($row['langcode']);
}
else {
$entity = $row['entity'];
}
if ($entity && $entity->hasField($field_name)) {
$entity->set($field_name, $row['weight']);
$entity->save();
}
}
}
}

9
tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml

@ -0,0 +1,9 @@
name: 'Integer weight test views'
type: module
description: 'Provides default views for integer weight views tests.'
package: Testing
core: 8.x
dependencies:
- drupal:node
- drupal:views
- drupal:language

251
tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml

@ -0,0 +1,251 @@
langcode: en
status: true
dependencies:
config:
- node.type.repo_item
module:
- node
- user
id: test_integer_weight
label: 'test integer weight'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: table
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
field_integer_weight:
id: field_integer_weight
table: node__field_integer_weight
field: field_integer_weight
relationship: none
group_type: group
admin_label: ''
label: 'Integer weight selector (field_integer_weight)'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
range: '20'
plugin_id: integer_weight_selector
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
type:
id: type
table: node_field_data
field: type
value:
repo_item:repo_item
entity_type: node
entity_field: type
plugin_id: bundle
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
field_integer_weight_value:
id: field_integer_weight_value
table: node__field_integer_weight
field: field_integer_weight_value
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
plugin_id: standard
title: 'test weight'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-integer-weight
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

201
tests/src/FunctionalJavascript/IntegerWeightTest.php

@ -0,0 +1,201 @@
<?php
namespace Drupal\Tests\islandora\FunctionalJavascript;
use Behat\Mink\Exception\ExpectationException;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\Node;
/**
* Test integer weight selector.
*
* Taken from the weight module with some edits.
*
* @group islandora
*/
class IntegerWeightTest extends WebDriverTestBase {
use FieldUiTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node',
'views',
'field_ui',
'integer_weight_test_views',
];
/**
* Name of the field.
*
* Used in the test view; change there
* if changed here.
*
* @var string
*/
protected static $fieldName = 'field_integer_weight';
/**
* Type of the field.
*
* @var string
*/
protected static $fieldType = 'integer';
/**
* User that can edit content types.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public static $testViews = [
'test_integer_weight',
];
/**
* Array of nodes to test with.
*
* @var array
*/
public $nodes = [];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(
[
'administer content types',
'administer node fields',
'administer node display',
]
);
// Create dummy repo_item type to sort (since we don't have
// repository_object without islandora_defaults).
$type = $this->container->get('entity_type.manager')->getStorage('node_type')
->create([
'type' => 'repo_item',
'name' => 'Repository Item',
]);
$type->save();
$this->container->get('router.builder')->rebuild();
$fieldStorage = FieldStorageConfig::create([
'fieldName' => static::$fieldName,
'entity_type' => 'node',
'type' => static::$fieldType,
]);
$fieldStorage->save();
$field = FieldConfig::create([
'field_storage' => $fieldStorage,
'bundle' => 'repo_item',
'required' => FALSE,
]);
$field->save();
for ($n = 1; $n <= 3; $n++) {
$node = $this->drupalCreateNode([
'type' => 'repo_item',
'title' => "Item $n",
static::$fieldName => $n,
]);
$node->save();
$this->nodes[] = $node;
}
ViewsTestData::createTestViews(get_class($this), ['integer_weight_test_views']);
}
/**
* Test integer weight selector.
*/
public function testIntegerWeightSelector() {
$this->drupalGet('test-integer-weight');
$page = $this->getSession()->getPage();
$weight_select1 = $page->findField("field_integer_weight[0][weight]");
$weight_select2 = $page->findField("field_integer_weight[1][weight]");
$weight_select3 = $page->findField("field_integer_weight[2][weight]");
// Are row weight selects hidden?
$this->assertFalse($weight_select1->isVisible());
$this->assertFalse($weight_select2->isVisible());
$this->assertFalse($weight_select3->isVisible());
// Check that 'Item 2' is feavier than 'Item 1'.
$this->assertGreaterThan($weight_select1->getValue(), $weight_select2->getValue());
// Does 'Item 1' preced 'Item 2'?
$this->assertOrderInPage(['Item 1', 'Item 2']);
// No changes yet, so no warning...
$this->assertSession()->pageTextNotContains('You have unsaved changes.');
// Drag and drop 'Item 1' over 'Item 2'.
$dragged = $this->xpath("//tr[@class='draggable'][1]//a[@class='tabledrag-handle']")[0];
$target = $this->xpath("//tr[@class='draggable'][2]//a[@class='tabledrag-handle']")[0];
$dragged->dragTo($target);
// Pause for javascript to do it's thing.
$this->assertJsCondition('jQuery(".tabledrag-changed-warning").is(":visible")');
// Look for unsaved changes warning.
$this->assertSession()->pageTextContains('You have unsaved changes.');
// 'Item 2' should now preced 'Item 1'.
$this->assertOrderInPage(['Item 2', 'Item 1']);
$this->submitForm([], 'Save');
// Form refresh should reflect the new order still.
$this->assertOrderInPage(['Item 2', 'Item 1']);
// Ensure the stored values reflect the new order.
$item1 = Node::load($this->nodes[0]->id());
$item2 = Node::load($this->nodes[1]->id());
$this->assertGreaterThan($item2->field_integer_weight->getString(), $item1->field_integer_weight->getString());
}
/**
* Asserts that several pieces of markup are in a given order in the page.
*
* Taken verbatim from the weight module.
*
* @param string[] $items
* An ordered list of strings.
*
* @throws \Behat\Mink\Exception\ExpectationException
* When any of the given string is not found.
*/
protected function assertOrderInPage(array $items) {
$session = $this->getSession();
$text = $session->getPage()->getHtml();
$strings = [];
foreach ($items as $item) {
if (($pos = strpos($text, $item)) === FALSE) {
throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver());
}
$strings[$pos] = $item;
}
ksort($strings);
$ordered = implode(', ', array_map(function ($item) {
return "'$item'";
}, $items));
$this->assertSame($items, array_values($strings), "Found strings, ordered as: $ordered.");
}
}
Loading…
Cancel
Save