diff --git a/CHANGELOG.md b/CHANGELOG.md index b513b32a..11bed8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Nedenfor ses dato for release og beskrivelse af opgaver som er implementeret. ## [Under udvikling] +* Tilføjede mulighed for ændring af ejerskab på node [PR-421](https://github.com/itk-dev/os2forms_selvbetjening/pull/421). + ## [4.2.1] 2025-05-06 * Sikrede at OS2Forms attachment elementer bliver detekteret korrekt diff --git a/composer.json b/composer.json index 752d10a6..b9ab3c3d 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,8 @@ "require": { "php": "^8.3", "composer/installers": "^2.0", + "drupal/author_bulk_assignment": "^2.0", + "drupal/chosen": "^5.0", "drupal/ckeditor": "^1.0", "drupal/clamav": "^2.0", "drupal/color": "^1.0", diff --git a/composer.lock b/composer.lock index 12dd5522..390a2ec2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ba4fba1f87c766ea7a46bfa629dbaaf0", + "content-hash": "2de54324c83dcd3b3d49fe7193b0afa5", "packages": [ { "name": "asm89/stack-cors", @@ -1548,6 +1548,52 @@ "source": "https://git.drupalcode.org/project/advancedqueue" } }, + { + "name": "drupal/author_bulk_assignment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/author_bulk_assignment.git", + "reference": "2.0.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/author_bulk_assignment-2.0.0.zip", + "reference": "2.0.0", + "shasum": "c055a2e21e9bd592662bfc66ec0b9c5c6537212f" + }, + "require": { + "drupal/core": "^10 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.0.0", + "datestamp": "1727438654", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Fathima Asmat", + "homepage": "https://www.drupal.org/u/fathimaasmat", + "role": "Maintainer" + } + ], + "description": "Author bulk assignment Drupal module", + "homepage": "https://www.drupal.org/project/author_bulk_assignment", + "support": { + "source": "https://git.drupalcode.org/project/author_bulk_assignment", + "issues": "https://www.drupal.org/project/issues/author_bulk_assignment" + } + }, { "name": "drupal/authorization", "version": "1.4.0", @@ -1658,6 +1704,149 @@ "source": "https://git.drupalcode.org/project/cache_control_override" } }, + { + "name": "drupal/chosen", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/chosen.git", + "reference": "5.0.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/chosen-5.0.2.zip", + "reference": "5.0.2", + "shasum": "bda191e28f3461bfa36f63d12b6d14c346104840" + }, + "require": { + "drupal/chosen_lib": "*", + "drupal/core": "^10.2 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "5.0.2", + "datestamp": "1746293867", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "aidanlis", + "homepage": "https://www.drupal.org/user/502018" + }, + { + "name": "Cyclodex", + "homepage": "https://www.drupal.org/user/1305230" + }, + { + "name": "dave reid", + "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "hydra", + "homepage": "https://www.drupal.org/user/647364" + }, + { + "name": "kalman.hosszu", + "homepage": "https://www.drupal.org/user/267481" + }, + { + "name": "nagy.balint", + "homepage": "https://www.drupal.org/user/1763952" + }, + { + "name": "pol", + "homepage": "https://www.drupal.org/user/47194" + }, + { + "name": "shadcn", + "homepage": "https://www.drupal.org/user/571032" + }, + { + "name": "supercabbageuk", + "homepage": "https://www.drupal.org/user/235438" + } + ], + "description": "Makes select elements more user-friendly using Chosen.", + "homepage": "https://www.drupal.org/project/chosen", + "support": { + "source": "https://git.drupalcode.org/project/chosen" + } + }, + { + "name": "drupal/chosen_lib", + "version": "5.0.2", + "require": { + "drupal/chosen": "^5", + "drupal/core": "^10.2 || ^11" + }, + "type": "metapackage", + "extra": { + "drupal": { + "version": "5.0.2", + "datestamp": "1746293867", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "aidanlis", + "homepage": "https://www.drupal.org/user/502018" + }, + { + "name": "Cyclodex", + "homepage": "https://www.drupal.org/user/1305230" + }, + { + "name": "dave reid", + "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "hydra", + "homepage": "https://www.drupal.org/user/647364" + }, + { + "name": "kalman.hosszu", + "homepage": "https://www.drupal.org/user/267481" + }, + { + "name": "nagy.balint", + "homepage": "https://www.drupal.org/user/1763952" + }, + { + "name": "pol", + "homepage": "https://www.drupal.org/user/47194" + }, + { + "name": "shadcn", + "homepage": "https://www.drupal.org/user/571032" + }, + { + "name": "supercabbageuk", + "homepage": "https://www.drupal.org/user/235438" + } + ], + "description": "Add chosen library definition.", + "homepage": "https://www.drupal.org/project/chosen", + "support": { + "source": "https://git.drupalcode.org/project/chosen" + } + }, { "name": "drupal/ckeditor", "version": "1.0.2", diff --git a/config/sync/advancedqueue.advancedqueue_queue.os2forms_queued_email.yml b/config/sync/advancedqueue.advancedqueue_queue.os2forms_queued_email.yml deleted file mode 100644 index ac69f369..00000000 --- a/config/sync/advancedqueue.advancedqueue_queue.os2forms_queued_email.yml +++ /dev/null @@ -1,22 +0,0 @@ -uuid: ba4856c1-a666-4cd0-a506-1fa08bb66cd4 -langcode: da -status: true -dependencies: - enforced: - module: - - os2forms_queued_email -_core: - default_config_hash: N9qjjFEXcXH9WSKJH1J8zJ0v5TnB5KfvwBM_LY_Fd90 -id: os2forms_queued_email -label: 'OS2forms queued e-mail' -backend: database -backend_configuration: - lease_time: 300 -processor: daemon -processing_time: 280 -threshold: - type: 0 - limit: 0 - state: all -locked: false -stop_when_empty: true diff --git a/config/sync/author_bulk_assignment.settings.yml b/config/sync/author_bulk_assignment.settings.yml new file mode 100644 index 00000000..f9960ad5 --- /dev/null +++ b/config/sync/author_bulk_assignment.settings.yml @@ -0,0 +1,2 @@ +types: + - node diff --git a/config/sync/chosen.settings.yml b/config/sync/chosen.settings.yml new file mode 100644 index 00000000..e0ddf122 --- /dev/null +++ b/config/sync/chosen.settings.yml @@ -0,0 +1,20 @@ +_core: + default_config_hash: zDwqaWPY76K_8TgX6aS789W7nZIEg7lU9MJsoh61bAU +langcode: da +minimum_single: 20 +minimum_multiple: 20 +disable_search_threshold: 0 +minimum_width: 0 +max_shown_results: null +use_relative_width: false +jquery_selector: 'select:visible' +search_contains: false +disable_search: false +allow_single_deselect: false +allow_mobile: false +add_helper_buttons: false +disabled_themes: { } +chosen_include: 2 +placeholder_text_multiple: 'Choose some options' +placeholder_text_single: 'Choose an option' +no_results_text: 'No results match' diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index 79d16c94..f75d5b0f 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -5,12 +5,15 @@ module: admin_toolbar: 0 admin_toolbar_tools: 0 advancedqueue: 0 + author_bulk_assignment: 0 automated_cron: 0 beskedfordeler: 0 big_pipe: 0 block: 0 block_content: 0 breakpoint: 0 + chosen: 0 + chosen_lib: 0 ckeditor: 0 ckeditor5: 0 clamav: 0 @@ -95,7 +98,6 @@ module: os2forms_payment: 0 os2forms_permission_alterations: 0 os2forms_permissions_by_term: 0 - os2forms_queued_email: 0 os2forms_rest_api: 0 os2forms_sbsys: 0 os2forms_selvbetjening: 0 diff --git a/config/sync/language/en/chosen.settings.yml b/config/sync/language/en/chosen.settings.yml new file mode 100644 index 00000000..4caab3a3 --- /dev/null +++ b/config/sync/language/en/chosen.settings.yml @@ -0,0 +1,3 @@ +placeholder_text_multiple: 'Choose some options' +placeholder_text_single: 'Choose an option' +no_results_text: 'No results match' diff --git a/config/sync/system.action.node_author_bulk_assignment_action.yml b/config/sync/system.action.node_author_bulk_assignment_action.yml new file mode 100644 index 00000000..b8023ea5 --- /dev/null +++ b/config/sync/system.action.node_author_bulk_assignment_action.yml @@ -0,0 +1,12 @@ +uuid: d851dc6b-d59c-48f1-a4f0-9eb8ab62d01b +langcode: da +status: true +dependencies: + module: + - author_bulk_assignment + - node +id: node_author_bulk_assignment_action +label: 'Assign bulk indholdselement to author' +type: node +plugin: 'entity:author_bulk_assignment_action:node' +configuration: { } diff --git a/config/sync/user.role.site_admin.yml b/config/sync/user.role.site_admin.yml index e9e69864..d285f0d7 100644 --- a/config/sync/user.role.site_admin.yml +++ b/config/sync/user.role.site_admin.yml @@ -8,6 +8,7 @@ dependencies: - node.type.webform - workflows.workflow.udgivelse_af_forlob_og_webformularer module: + - author_bulk_assignment - block_content - content_moderation - contextual @@ -68,6 +69,7 @@ permissions: - 'administer webform revisions' - 'administer webform submission' - 'administer webform templates' + - 'assign author to selected content' - 'bypass entity print access' - 'bypass honeypot protection' - 'can be workflow participant' diff --git a/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.info.yml b/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.info.yml index 83d3e8f5..26ab7cca 100644 --- a/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.info.yml +++ b/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.info.yml @@ -5,3 +5,4 @@ package: 'OS2Forms' core_version_requirement: ^9 || ^10 dependencies: - 'webform:webform' + - 'chosen:chosen' diff --git a/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.module b/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.module index 0a19ab20..aedf8486 100644 --- a/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.module +++ b/web/modules/custom/os2forms_selvbetjening/os2forms_selvbetjening.module @@ -27,3 +27,14 @@ function os2forms_selvbetjening_webform_element_alter(array &$element, FormState $element['#attributes']['class'][] = 'js-form-item'; } } + +/** + * Implements hook_views_plugins_field_alter(). + */ +function os2forms_selvbetjening_views_plugins_field_alter(array &$plugins) { + if (array_key_exists('author_assignment_node_bulk_form', $plugins)) { + $plugins['author_assignment_node_bulk_form']['id'] .= '_override'; + $plugins['author_assignment_node_bulk_form']['class'] = 'Drupal\os2forms_selvbetjening\Plugin\views\field\AuthorAssignmentNodeBulkFormOverride'; + $plugins['author_assignment_node_bulk_form']['provider'] = 'os2forms_selvbetjening'; + } +} diff --git a/web/modules/custom/os2forms_selvbetjening/src/Plugin/views/field/AuthorAssignmentNodeBulkFormOverride.php b/web/modules/custom/os2forms_selvbetjening/src/Plugin/views/field/AuthorAssignmentNodeBulkFormOverride.php new file mode 100644 index 00000000..eb54414b --- /dev/null +++ b/web/modules/custom/os2forms_selvbetjening/src/Plugin/views/field/AuthorAssignmentNodeBulkFormOverride.php @@ -0,0 +1,227 @@ +langcode = $language_manager->getCurrentLanguage()->getId(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + /** @var static */ + return new self( + $container->get('permissions_by_term.access_storage'), + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('language_manager'), + $container->get('messenger'), + $container->get('entity.repository'), + $container->get('current_user') + ); + } + + /** + * Gets the taxonomy terms that the current user has access to. + * + * @return array + * An array of term IDs that the current user has permission to access. + * Returns an empty array if no terms are accessible or if the user + * has no permissions. + */ + private function getUserTerms(): array { + $currentUser = User::load($this->currentUser->id()); + + return $this->accessStorage->getPermittedTids($currentUser->id(), $currentUser->getRoles()); + } + + /** + * Retrieves a list of users based on term IDs. + * + * @param array $userTermsIds + * An array of term IDs for which to retrieve the associated users. + * + * @return array + * An associative array where the keys are user IDs and the values are + * the display names of the users. Returns an empty array if no users + * are found. + */ + private function getUsersByTermId(array $userTermsIds): array { + $users = []; + if (!empty($userTermsIds)) { + // Get all users that have access to these terms. + foreach ($userTermsIds as $termId) { + $userIdsResult = $this->accessStorage->getAllowedUserIds($termId, $this->langcode); + foreach ($userIdsResult as $userId) { + if (!isset($users[$userId])) { + if ($user = User::load($userId)) { + $users[$userId] = $user->getDisplayName(); + } + } + } + } + } + return $users; + } + + /** + * Filters users by their access to webform terms. + * + * @param array $users + * An associative array of users where the key is the user ID and + * the value is the username. + * @param array $terms + * An array of terms or term groups defining access requirements. + * + * @return array + * A filtered list of users who have access to the given terms. + */ + private function filterUsersByWebformAccess(array $users, array $terms): array { + $mergedTerms = array_values($terms); + $mergedTerms = !empty($mergedTerms) ? (is_array($mergedTerms[0]) ? array_merge(...$mergedTerms) : $mergedTerms) : []; + + return array_filter($users, function ($userName, $userId) use ($mergedTerms) { + $user = User::load($userId); + $userTermsIds = $this->accessStorage->getPermittedTids($user->id(), $user->getRoles()); + + // Check if all terms from $mergedTerms exist in $userTermsIds. + return empty($mergedTerms) || count(array_intersect($userTermsIds, $mergedTerms)) === count($mergedTerms); + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * {@inheritdoc} + */ + public function viewsForm(&$form, FormStateInterface $form_state): void { + parent::viewsForm($form, $form_state); + + $form['header']['node_bulk_form']['assignee_uid']['#type'] = 'select'; + $form['header']['node_bulk_form']['assignee_uid']['#chosen'] = TRUE; + $form['header']['node_bulk_form']['action']['#options']['node_author_bulk_assignment_action'] = $this->t('Change ownership'); + + $userTermsIds = $this->getUserTerms(); + $users = $this->getUsersByTermId($userTermsIds); + $form_state->set('users', $users); + + $form['header']['node_bulk_form']['assignee_uid']['#options'] = $users; + } + + /** + * Validates the bulk form submission for assigning nodes. + * + * @param array &$form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function viewsFormValidate(&$form, FormStateInterface $form_state): void { + parent::viewsFormValidate($form, $form_state); + + $users = $form_state->get('users'); + $user_input = $form_state->getUserInput(); + $selected = array_filter($user_input[$this->options['id']]); + $webformPermissionsByTermArray = []; + + foreach ($selected as $bulk_form_key) { + $webform = NULL; + $entity = $this->loadEntityFromBulkFormKey($bulk_form_key); + if ($entity instanceof NodeInterface && $entity->hasField('webform')) { + $webform = $entity->get('webform')->entity; + } + + if ($webform instanceof Webform) { + $webformPermissionsByTerm = $webform->getThirdPartySetting('os2forms_permissions_by_term', 'settings'); + $webformPermissionsByTermArray = $this->addUniquePermissions( + $webformPermissionsByTermArray, + $webformPermissionsByTerm + ); + + } + else { + $form_state->setErrorByName('AuthorAssignmentNodeBulkFormError', $this->t('One or more of the selected nodes does not have a webform connected to it.')); + } + } + + $filteredUsers = $this->filterUsersByWebformAccess($users, $webformPermissionsByTermArray); + $selectedAssignee = $form_state->getValue('assignee_uid'); + + if (!isset($filteredUsers[$selectedAssignee]) && $selectedAssignee != 0) { + $form_state->setErrorByName('AuthorAssignmentNodeBulkFormError', $this->t('The selected user does not have access to one or more of the selected webforms.')); + } + } + + /** + * Adds unique permissions into the existing permissions array. + * + * @param array $existingPermissions + * The current permissions array. + * @param array $newPermissions + * Permissions to merge. + * + * @return array + * The merged and filtered permissions array + */ + private function addUniquePermissions(array $existingPermissions, array $newPermissions): array { + $uniquePermissions = array_filter( + $newPermissions, + fn($value, $key) => $value == $key, + ARRAY_FILTER_USE_BOTH + ); + + return array_values(array_unique( + array_merge($existingPermissions, $uniquePermissions) + )); + } + +}