Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4f9b7d0
Merge branch 'Dolibarr:develop' into develop
mapiolca Feb 19, 2026
2a21f0c
Merge branch 'Dolibarr:develop' into develop
mapiolca Mar 4, 2026
e972856
Merge branch 'Dolibarr:develop' into develop
mapiolca Mar 16, 2026
fd761ac
Merge branch 'Dolibarr:develop' into develop
mapiolca Mar 23, 2026
09071ee
Merge branch 'Dolibarr:develop' into develop
mapiolca Mar 24, 2026
5f204e3
refactor(project): move task pre-massaction dialogs to massactions_pr…
mapiolca Mar 25, 2026
4f8d328
chore(i18n): remove added task massaction keys from non-en locales
mapiolca Mar 25, 2026
069e1e2
refactor(project): simplify task massaction routing logic
mapiolca Mar 25, 2026
6804fda
ui(project): auto-size task massaction modal and center in viewport
mapiolca Mar 25, 2026
ff9f5ae
fix(project): ensure task massaction final action is posted and proce…
mapiolca Mar 25, 2026
802c103
fix(project): post final task massaction key and add debug tracing
mapiolca Mar 25, 2026
753af7e
fix(project): add warning-level logs for task massaction flow
mapiolca Mar 25, 2026
31db070
fix(project): add detailed massaction runtime traces
mapiolca Mar 25, 2026
df5ea6b
fix(project): submit per-task modal fields in ajax formconfirm
mapiolca Mar 25, 2026
1c34b7a
feat(project): expose new task massactions on project tasks page
mapiolca Mar 25, 2026
61953a1
fix(project): keep project id in task massaction modal submit url
mapiolca Mar 25, 2026
47c361d
ui(project): add global datetime apply row in task date massaction modal
mapiolca Mar 25, 2026
d60e1ae
ui(project): add 5px buffer to task massaction modal body height
mapiolca Mar 25, 2026
abd617a
ui(project): increase modal body extra height buffer to 10px
mapiolca Mar 25, 2026
ef25b6f
ui(project): compute modal body height from task rows and fixed heade…
mapiolca Mar 25, 2026
03c2e99
ui(project): add 15px extra body height to task massaction modal
mapiolca Mar 25, 2026
55600f9
ui(project): increase modal body extra offset to +25px
mapiolca Mar 25, 2026
8a7ab64
ui(project): increase precomputed modal height budget before display
mapiolca Mar 25, 2026
20daed9
ui(project): reduce modal body extra offset by 10px
mapiolca Mar 25, 2026
24e368e
ui(project): reduce modal body extra offset by 5px
mapiolca Mar 25, 2026
933df90
ui(project): reduce modal body extra offset to +5px
mapiolca Mar 25, 2026
06db76f
Update massactions_pre.tpl.php
mapiolca Mar 25, 2026
328699e
Update task.class.php
mapiolca Mar 25, 2026
4682845
Update list.php
mapiolca Mar 25, 2026
f8e61c2
Doc
eldy Mar 25, 2026
b6375f9
Update tasks.php
mapiolca Mar 25, 2026
b2d4c9e
Merge branch 'develop' into 2026-03-25-add-mass-actions-for-external-…
mapiolca Mar 25, 2026
f53e4d3
fix(project tasks): align massaction labels and indentation
mapiolca Mar 25, 2026
fc7c0d5
Merge branch '2026-03-25-add-mass-actions-for-external-module-tasks' …
mapiolca Mar 25, 2026
570fb01
Merge pull request #36 from mapiolca/2026-03-25-add-mass-actions-for-…
mapiolca Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion htdocs/conf/conf.php.example
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,26 @@ $dolibarr_cron_allow_cli='0';
// 2 will allow custom PHP inside website features even if your PHP setup does not protect you against dangerous system calls (this may be dangerous if you don't have a RCE protection like SELnux or Apparmor).
// Default value: '0'
// Examples: '0', '1' or '2'

//
// $dolibarr_website_allow_custom_php='0';

// dolibarr_allow_localurl_for_webhooks
// ====================================
// Allow webhooks to use a local url
// Default value: '0'
// Examples: '1'
//
// $dolibarr_allow_localurl_for_webhooks = '0';

// dolibarr_allow_unsecured_select_in_extrafields_filter
// =====================================================
// Allow the use of subrequests inside USF IN filters
// Default value: '0'
// Examples: '1'
//
// $dolibarr_allow_unsecured_select_in_extrafields_filter = '0';



// php_session_save_handler
// ========================
Expand Down
122 changes: 122 additions & 0 deletions htdocs/core/tpl/massactions_pre.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright (C) 2024-2025 MDW <[email protected]>
* Copyright (C) 2024 Frédéric France <[email protected]>
* Copyright (C) 2024 Ferran Marcet <[email protected]>
* Copyright (C) 2026 Pierre Ardoin <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -94,6 +95,127 @@
print $form->formconfirm($_SERVER['PHP_SELF'] . '?id=' . $object->id . $selected, $langs->trans('ConfirmMassClone'), '', 'clonetasks', $formquestion, '', 1, 300, 590);
}

if (in_array($massaction, array('preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true) && is_object($objecttmp) && $objecttmp->element == 'project_task') {
dol_syslog(__FILE__." render pre-massaction modal for massaction='".$massaction."' selected=".count((array) $toselect), LOG_WARNING);
if (!$user->hasRight('projet', 'creer')) {
setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors');
} else {
$tasksById = $objecttmp->getAuthorizedTasksForMassAction($user, $toselect);
dol_syslog(__FILE__." pre-massaction authorized tasks=".count($tasksById), LOG_WARNING);
if (empty($tasksById)) {
setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings');
} else {
if ($massaction == 'preupdate_selected_tasks_progress') {
$finalAction = 'update_selected_tasks_progress';
} elseif ($massaction == 'preupdate_selected_tasks_start_date') {
$finalAction = 'update_selected_tasks_start_date';
} else {
$finalAction = 'update_selected_tasks_deadline';
}
$rowCount = count($tasksById);
$isDateAction = ($finalAction != 'update_selected_tasks_progress');
$taskRowsHeight = $rowCount * 34;
$tableHeaderHeight = 34;
$updateTasksRowHeight = ($isDateAction ? 36 : 0);
$confirmQuestionRowHeight = 34;
$computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 5;
$modalBodyHeight = min(700, max(220, $computedBodyHeight));
$modalHeight = min(900, $modalBodyHeight + 220);
$formquestion = array();
$currentProjectId = GETPOSTINT('id');
if ($currentProjectId > 0) {

Check failure on line 126 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 3 tabs, found 4
$formquestion[] = array('type' => 'hidden', 'name' => 'id', 'value' => $currentProjectId);
}

Check failure on line 128 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 3 tabs, found 4
$formquestion[] = array('type' => 'hidden', 'name' => 'massactiontaskfinal', 'value' => $finalAction);
$formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById)));

if ($finalAction == 'update_selected_tasks_progress') {

Check failure on line 132 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 3 tabs, found 4
$progressInputNames = array();
$tablehtml = '<div id="project_task_massaction_modal_wrapper" data-body-height="'.((int) $modalBodyHeight).'" data-modal-height="'.((int) $modalHeight).'"><table class="noborder centpercent">';
$tablehtml .= '<tr class="liste_titre"><th>'.$langs->trans('Task').'</th><th class="right">'.$langs->trans('Progress').'</th></tr>';
foreach ($tasksById as $taskId => $taskDb) {

Check failure on line 136 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 4 tabs, found 5
$inputname = 'task_progress_'.$taskId;
$progressInputNames[] = $inputname;
$tablehtml .= '<tr><td>'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'</td>';
$tablehtml .= '<td class="right"><input type="text" class="flat right width75" maxlength="6" id="'.dol_escape_htmltag($inputname).'" name="'.dol_escape_htmltag($inputname).'" value="'.((string) min(100, max(0, (int) $taskDb->progress))).'"> %</td></tr>';
}

Check failure on line 141 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 4 tabs, found 5
$tablehtml .= '</table></div>';
$formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml);
$titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
} else {

Check failure on line 145 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 3 tabs, found 4
$dateInputNames = array();
$keepDurationNames = array();
$tablehtml = '<div id="project_task_massaction_modal_wrapper" data-body-height="'.((int) $modalBodyHeight).'" data-modal-height="'.((int) $modalHeight).'">';
$tablehtml .= '<div class="opacitymedium margintoponly marginbottomonly">';
$tablehtml .= '<span class="marginright">'.$langs->trans('MassActionApplyDateToTasks').'</span>';
$tablehtml .= '<input type="datetime-local" class="flat marginright" id="global_task_datetime" value="">';
$tablehtml .= '<input type="button" class="button button-small smallpaddingimp" id="apply_global_task_datetime" value="'.dol_escape_htmltag($langs->trans('Apply')).'">';
$tablehtml .= '</div>';
$tablehtml .= '<table class="noborder centpercent">';
$tablehtml .= '<tr class="liste_titre"><th>'.$langs->trans('Task').'</th><th class="center">'.$langs->trans('DateHour').'</th><th class="center">'.$langs->trans('Duration').'</th></tr>';

Check failure on line 155 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Missing translation; $tablehtml .= '<tr class="liste_titre"><th>'.$langs->trans('Task').'</th><th class="center">'.$langs->trans('DateHour').'</th><th class="center">'.$langs->trans('Duration').'</th></tr>';
foreach ($tasksById as $taskId => $taskDb) {

Check failure on line 156 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 4 tabs, found 5
$datetimeName = 'task_datetime_'.$taskId;
$keepdurationname = 'keep_duration_'.$taskId;
$dateInputNames[] = $datetimeName;
$keepDurationNames[] = $keepdurationname;
if ($finalAction == 'update_selected_tasks_start_date') {

Check failure on line 161 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 5 tabs, found 6
$currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now());
} else {

Check failure on line 163 in htdocs/core/tpl/massactions_pre.tpl.php

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

Line indented incorrectly; expected 5 tabs, found 6
$currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now());
}
$datetimevalue = dol_print_date($currenttimestamp, '%Y-%m-%dT%H:%M');
$tablehtml .= '<tr><td>'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'</td>';
$tablehtml .= '<td class="center"><input type="datetime-local" class="flat" id="'.dol_escape_htmltag($datetimeName).'" name="'.dol_escape_htmltag($datetimeName).'" value="'.dol_escape_htmltag($datetimevalue).'"></td>';
$tablehtml .= '<td class="center"><input type="checkbox" class="flat" id="'.dol_escape_htmltag($keepdurationname).'" name="'.dol_escape_htmltag($keepdurationname).'" value="1"></td></tr>';
}
$tablehtml .= '</table></div>';
$formquestion[] = array('type' => 'other', 'name' => implode(',', array_merge($dateInputNames, $keepDurationNames)), 'label' => '', 'value' => $tablehtml);
$titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline'));
}

$pageforconfirm = $_SERVER['PHP_SELF'];
if ($currentProjectId > 0) {
$pageforconfirm .= (strpos($pageforconfirm, '?') === false ? '?' : '&').'id='.$currentProjectId;
}
print $form->formconfirm($pageforconfirm, $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, $modalHeight, 800, 0, 'Validate', 'Cancel');
print '<script nonce="'.getNonce().'">
(function() {
function adjustProjectTaskMassactionModal() {
var jqWrapper = jQuery("#project_task_massaction_modal_wrapper");
if (!jqWrapper.length) {
return;
}
var jqDialogContent = jqWrapper.closest(".ui-dialog-content");
if (!jqDialogContent.length || typeof jqDialogContent.dialog !== "function") {
return;
}
var targetBodyHeight = parseInt(jqWrapper.attr("data-body-height"), 10) || 320;
var targetModalHeight = parseInt(jqWrapper.attr("data-modal-height"), 10) || 520;
var maxModalHeight = Math.max(220, jQuery(window).height() - 100);
var finalModalHeight = Math.min(targetModalHeight, maxModalHeight);
var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight, finalModalHeight - 120));
jqDialogContent.css("max-height", finalBodyHeight + "px");
jqDialogContent.css("overflow-y", "auto");
jqDialogContent.dialog("option", "height", finalModalHeight);
jqDialogContent.dialog("option", "position", { my: "center", at: "center", of: window });
}
jQuery(document).ready(function() {
adjustProjectTaskMassactionModal();
jQuery(window).on("resize", adjustProjectTaskMassactionModal);
jQuery(document).on("click", "#apply_global_task_datetime", function() {
var globalValue = jQuery("#global_task_datetime").val();
if (typeof globalValue === "undefined" || globalValue === "") {
return;
}
jQuery("input[id^=\'task_datetime_\']").val(globalValue);
});
});
})();
</script>';
}
}
}

if ($massaction == 'preaffecttag' && isModEnabled('category')) {
require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
$categ = new Categorie($db);
Expand Down
13 changes: 13 additions & 0 deletions htdocs/langs/en_US/projects.lang
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,16 @@ TaskHourlyRateUpdated=Task's hourly rate updated
UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user
UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user
EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)...
MassActionCloseSelectedTasks=Close selected project tasks
MassActionUpdateSelectedTasksProgress=Change progress of selected project tasks
MassActionUpdateSelectedTasksStartDate=Change start date of selected project tasks
MassActionUpdateSelectedTasksDeadline=Change deadline of selected project tasks
MassActionKeepTaskDuration=Keep duration
MassActionTaskPopupDescription=Confirm the mass action on selected tasks.
MassActionSelectedTasksClosed=%s task(s) closed.
MassActionSelectedTasksProgressUpdated=%s task(s) progress updated.
MassActionSelectedTasksStartDateUpdated=%s task(s) start date updated.
MassActionSelectedTasksDeadlineUpdated=%s task(s) deadline updated.
MassActionInvalidTaskProgressValue=Invalid progress value for task ID %s.
MassActionInvalidTaskDateValue=Invalid date value for task ID %s.
MassActionApplyDateToTasks=Update tasks below
42 changes: 42 additions & 0 deletions htdocs/projet/class/task.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Copyright (C) 2023 Gauthier VERDOL <[email protected]>
* Copyright (C) 2024-2025 MDW <[email protected]>
* Copyright (C) 2024 Vincent de Grandpré <[email protected]>
* Copyright (C) 2026 Pierre Ardoin <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -620,6 +621,47 @@ public function fetch($id, $ref = '', $loadparentdata = 0)
}
}

/**
* Return authorized tasks for selected ids.
*
* @param User $user Current user
* @param array $toselect List of selected task ids
* @return array<int,object> Tasks indexed by task id
*/
public function getAuthorizedTasksForMassAction($user, $toselect)
{
$toselect = array_unique(array_filter(array_map('intval', (array) $toselect)));
if (empty($toselect)) {
return array();
}

$projectstatic = new Project($this->db);
$projectlistfilter = '';
if (!$user->hasRight('projet', 'all', 'lire')) {
$projectsListId = $projectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0);
$projectlistfilter = " AND p.rowid IN (".$this->db->sanitize($projectsListId ? $projectsListId : '0').")";
}

$sql = "SELECT t.rowid, t.ref, t.label, t.dateo, t.datee, t.progress, t.fk_statut, t.fk_projet";
$sql .= " FROM ".MAIN_DB_PREFIX."projet_task AS t";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet AS p ON p.rowid = t.fk_projet";
$sql .= " WHERE t.rowid IN (".implode(',', $toselect).")";
$sql .= " AND p.entity IN (".getEntity('project').")";
$sql .= $projectlistfilter;

$resql = $this->db->query($sql);
if (!$resql) {
return array();
}

$tasksById = array();
while ($obj = $this->db->fetch_object($resql)) {
$tasksById[(int) $obj->rowid] = $obj;
}

return $tasksById;
}


/**
* Update database
Expand Down
Loading
Loading