diff --git a/islandora.links.action.yml b/islandora.links.action.yml index 7f1f299d..f8bf7d2b 100644 --- a/islandora.links.action.yml +++ b/islandora.links.action.yml @@ -9,3 +9,9 @@ islandora.add_member_to_node: title: Add child appears_on: - 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 diff --git a/islandora.module b/islandora.module index e39b4be4..dda56c8c 100644 --- a/islandora.module +++ b/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); + } + } +} diff --git a/islandora.views.inc b/islandora.views.inc new file mode 100644 index 00000000..cd826d08 --- /dev/null +++ b/islandora.views.inc @@ -0,0 +1,25 @@ +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'; + } + } +} diff --git a/modules/islandora_core_feature/config/install/views.view.reorder_children.yml b/modules/islandora_core_feature/config/install/views.view.reorder_children.yml new file mode 100644 index 00000000..b262141d --- /dev/null +++ b/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: { } diff --git a/src/EventSubscriber/AdminViewsRouteSubscriber.php b/src/EventSubscriber/AdminViewsRouteSubscriber.php index 0ae1626c..64367673 100644 --- a/src/EventSubscriber/AdminViewsRouteSubscriber.php +++ b/src/EventSubscriber/AdminViewsRouteSubscriber.php @@ -24,6 +24,10 @@ class AdminViewsRouteSubscriber extends RouteSubscriberBase { $route->setRequirement('_permission', 'manage members'); $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'); + } } } diff --git a/src/Plugin/views/field/IntegerWeightSelector.php b/src/Plugin/views/field/IntegerWeightSelector.php new file mode 100644 index 00000000..731ae4b3 --- /dev/null +++ b/src/Plugin/views/field/IntegerWeightSelector.php @@ -0,0 +1,101 @@ +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(); + } + } + } + +} diff --git a/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml b/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml new file mode 100644 index 00000000..78c510fc --- /dev/null +++ b/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 diff --git a/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml b/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml new file mode 100644 index 00000000..c08b8400 --- /dev/null +++ b/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: { } + diff --git a/tests/src/FunctionalJavascript/IntegerWeightTest.php b/tests/src/FunctionalJavascript/IntegerWeightTest.php new file mode 100644 index 00000000..ba3892e6 --- /dev/null +++ b/tests/src/FunctionalJavascript/IntegerWeightTest.php @@ -0,0 +1,201 @@ +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."); + } + +}