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)
+ ));
+ }
+
+}