Skip to content

Commit 72fa8b1

Browse files
authored
Adding bulk edit of group (#3366)
This new feature allows to select multiple builds in a group and move them as a batch or mark the builds expected. - Added fix when image for project is not set - Modified icons for better look and feel <img width="1800" height="509" alt="image" src="https://github.com/user-attachments/assets/96bb1c6c-fbfc-42d4-871b-0cf46d4b5d7a" />
1 parent dbe45fb commit 72fa8b1

9 files changed

Lines changed: 381 additions & 87 deletions

File tree

resources/js/angular/controllers/index.js

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function showEmptyBuildsLast() {
4747
};
4848
}
4949

50-
export function IndexController($scope, $rootScope, $location, $http, $filter, $timeout, anchors, apiLoader, filters, multisort, modalSvc) {
50+
export function IndexController($scope, $rootScope, $location, $http, $filter, $timeout, $q, anchors, apiLoader, filters, multisort, modalSvc) {
5151
// Show spinner while page is loading.
5252
$scope.loading = true;
5353

@@ -201,6 +201,17 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
201201
$scope.cdash.buildgroups[i].builds = $filter('orderBy')($scope.cdash.buildgroups[i].builds, $scope.cdash.buildgroups[i].orderByFields);
202202
$scope.cdash.buildgroups[i].builds = $filter('showEmptyBuildsLast')($scope.cdash.buildgroups[i].builds, $scope.cdash.buildgroups[i].orderByFields);
203203

204+
// Initialize expectedInGroup property for each build to avoid checkbox binding conflicts
205+
for (var j = 0; j < $scope.cdash.buildgroups[i].builds.length; j++) {
206+
$scope.cdash.buildgroups[i].builds[j].expectedInGroup = {};
207+
}
208+
209+
// Initialize bulk selection properties
210+
$scope.cdash.buildgroups[i].selectedBuilds = [];
211+
$scope.cdash.buildgroups[i].selectAll = false;
212+
$scope.cdash.buildgroups[i].bulkTargetGroup = '';
213+
$scope.cdash.buildgroups[i].selectionMode = false;
214+
204215
// Mark this group has having "normal" builds if it only contains missing & expected builds.
205216
if (!$scope.cdash.buildgroups[i].hasnormalbuilds && !$scope.cdash.buildgroups[i].hasparentbuilds && $scope.cdash.buildgroups[i].builds.length > 0) {
206217
$scope.cdash.buildgroups[i].hasnormalbuilds = true;
@@ -389,7 +400,7 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
389400
};
390401

391402
$scope.showModal = function (buildid) {
392-
modalSvc.showModal(buildid, $scope.removeBuild, 'modal-template');
403+
modalSvc.showModal(buildid, $scope.removeBuild, 'modal-template', null, 'sm', null, null, false);
393404
}
394405

395406
$scope.removeBuild = function(build) {
@@ -498,20 +509,133 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
498509
$http.post('api/v1/expectedbuild.php', parameters)
499510
.then(function success() {
500511
window.location.reload();
512+
}).catch(function(error) {
513+
console.error('Error moving expected build:', error);
514+
alert('An error occurred while moving the build. Please try again.');
501515
});
502516
} else {
517+
// Use the checkbox value for this specific group, default to current expected value
518+
var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[groupid] !== undefined
519+
? (build.expectedInGroup[groupid] ? 1 : 0)
520+
: build.expected;
521+
522+
// Use the build API with the correct parameters
503523
var parameters = {
504524
buildid: build.id,
505525
newgroupid: groupid,
506-
expected: build.expected
526+
expected: expectedInNewGroup
507527
};
508528
$http.post('api/v1/build.php', parameters)
509529
.then(function success() {
510530
window.location.reload();
531+
}).catch(function(error) {
532+
console.error('Error moving build:', error);
533+
alert('Error moving build: ' + (error.data && error.data.error ? error.data.error : 'Unknown error'));
511534
});
512535
}
513536
};
514537

538+
// Bulk selection functions
539+
$scope.toggleBuildSelection = function(build, buildgroup) {
540+
if (build.selected) {
541+
buildgroup.selectedBuilds.push(build);
542+
} else {
543+
var index = buildgroup.selectedBuilds.indexOf(build);
544+
if (index > -1) {
545+
buildgroup.selectedBuilds.splice(index, 1);
546+
}
547+
buildgroup.selectAll = false;
548+
}
549+
};
550+
551+
$scope.toggleSelectAll = function(buildgroup) {
552+
buildgroup.selectedBuilds = [];
553+
for (var i = 0; i < buildgroup.pagination.filteredBuilds.length; i++) {
554+
var build = buildgroup.pagination.filteredBuilds[i];
555+
if (build.id) { // Only select builds that have IDs (not expected missing builds)
556+
build.selected = buildgroup.selectAll;
557+
if (buildgroup.selectAll) {
558+
buildgroup.selectedBuilds.push(build);
559+
}
560+
}
561+
}
562+
};
563+
564+
$scope.clearBuildSelection = function(buildgroup) {
565+
buildgroup.selectAll = false;
566+
buildgroup.selectedBuilds = [];
567+
for (var i = 0; i < buildgroup.builds.length; i++) {
568+
buildgroup.builds[i].selected = false;
569+
}
570+
};
571+
572+
$scope.toggleSelectionMode = function(buildgroup) {
573+
buildgroup.selectionMode = !buildgroup.selectionMode;
574+
// Clear selection when exiting selection mode
575+
if (!buildgroup.selectionMode) {
576+
$scope.clearBuildSelection(buildgroup);
577+
}
578+
};
579+
580+
$scope.bulkMoveToGroup = function(buildgroup) {
581+
if (!buildgroup.bulkTargetGroup || buildgroup.selectedBuilds.length === 0) {
582+
return;
583+
}
584+
585+
var targetGroupId = parseInt(buildgroup.bulkTargetGroup, 10);
586+
var movePromises = [];
587+
588+
// Move each selected build using the build API
589+
for (var i = 0; i < buildgroup.selectedBuilds.length; i++) {
590+
var build = buildgroup.selectedBuilds[i];
591+
var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[targetGroupId] !== undefined
592+
? (build.expectedInGroup[targetGroupId] ? 1 : 0)
593+
: (build.expected || 0);
594+
595+
var parameters = {
596+
buildid: build.id,
597+
newgroupid: targetGroupId,
598+
expected: expectedInNewGroup
599+
};
600+
movePromises.push($http.post('api/v1/build.php', parameters));
601+
}
602+
603+
// Wait for all moves to complete, then reload
604+
$q.all(movePromises).then(function() {
605+
window.location.reload();
606+
}).catch(function(error) {
607+
console.error('Error moving builds:', error);
608+
alert('An error occurred while moving builds. Please try again.');
609+
});
610+
};
611+
612+
$scope.bulkMarkAsExpected = function(buildgroup, expectedValue) {
613+
if (buildgroup.selectedBuilds.length === 0) {
614+
return;
615+
}
616+
617+
var updatePromises = [];
618+
619+
// Update expected status for each selected build using the build API
620+
for (var i = 0; i < buildgroup.selectedBuilds.length; i++) {
621+
var build = buildgroup.selectedBuilds[i];
622+
var parameters = {
623+
buildid: build.id,
624+
groupid: parseInt(buildgroup.id, 10),
625+
expected: expectedValue
626+
};
627+
updatePromises.push($http.post('api/v1/build.php', parameters));
628+
}
629+
630+
// Wait for all updates to complete, then reload
631+
$q.all(updatePromises).then(function() {
632+
window.location.reload();
633+
}).catch(function(error) {
634+
console.error('Error updating builds:', error);
635+
alert('An error occurred while updating builds. Please try again.');
636+
});
637+
};
638+
515639
$scope.colorblind_toggle = function() {
516640
if ($scope.cdash.filterdata.colorblind) {
517641
$rootScope.cssfile = "colorblind";

resources/js/angular/legacy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { HeadController } from "./controllers/head";
5555
CDash.controller('HeadController', ["$rootScope", "$document", HeadController]);
5656

5757
import { IndexController, showEmptyBuildsLast } from "./controllers/index";
58-
CDash.controller('IndexController', ["$scope", "$rootScope", "$location", "$http", "$filter", "$timeout", "anchors", "apiLoader", "filters", "multisort", "modalSvc", IndexController]);
58+
CDash.controller('IndexController', ["$scope", "$rootScope", "$location", "$http", "$filter", "$timeout", "$q", "anchors", "apiLoader", "filters", "multisort", "modalSvc", IndexController]);
5959
CDash.filter('showEmptyBuildsLast', showEmptyBuildsLast);
6060

6161
import { SubProjectController } from "./controllers/subproject";

resources/js/angular/services/modal.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export function modalSvc ($uibModal) {
2-
const showModal = function(modelId, okFn, template, parent_scope, size, success, error) {
2+
const showModal = function(modelId, okFn, template, parent_scope, size, success, error, animation) {
33
parent_scope = typeof parent_scope !== 'undefined' ? parent_scope : null;
44
size = typeof size !== 'undefined' ? size : 'sm';
5+
animation = typeof animation !== 'undefined' ? animation : true;
56
var $modal = $uibModal.open({
6-
animation: true,
7+
animation: animation,
78
backdrop: true,
89
controller: function () {
910
var $ctrl = this;

resources/js/angular/views/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ <h3>
460460
<h4 class="modal-title">Remove Build</h4>
461461
</div>
462462
<div class="modal-body">
463-
<p>Are you sure you want to remove this Build?</p>
463+
<p>Are you sure you want to remove this build?</p>
464464
</div>
465465
<div class="modal-footer">
466466
<button class="btn" ng-click="$ctrl.cancel()">cancel</button>

resources/js/angular/views/partials/build.html

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
<!-- Selection checkbox for bulk actions (admin only) -->
2+
<td ng-if="cdash.user.admin == 1 && buildgroup.selectionMode" align="center" class="paddt">
3+
<input type="checkbox"
4+
ng-if="::build.id"
5+
ng-model="build.selected"
6+
ng-change="toggleBuildSelection(build, buildgroup)"
7+
data-cy="build-selection-checkbox">
8+
</td>
9+
110
<!-- For child view, display the label(s) as a link to the build summary -->
211
<td ng-if="::cdash.childview == 1" align="left" class="paddt" colspan="2">
312
<a class="cdash-link" ng-href="builds/{{::build.id}}">
@@ -7,7 +16,7 @@
716
<!-- Icon for build errors / test failing -->
817
<a class="cdash-link" ng-if="::build.compilation.error > 0 || build.test.fail > 0" href=""
918
ng-click="toggleBuildProblems(build)">
10-
<img src="img/Info.png" alt="info" class="icon"></img>
19+
<span class="glyphicon glyphicon-info-sign" style="color: #777;" title="Build information"></span>
1120
</a>
1221

1322
<!-- Link to notes specific to this subproject build -->
@@ -16,14 +25,14 @@
1625
name="notesLink"
1726
ng-if="::build.notes > 0"
1827
ng-href="builds/{{::build.id}}/notes">
19-
<img src="img/document.png" alt="Notes" class="icon"/>
28+
<span class="glyphicon glyphicon-file" style="color: #777;"></span>
2029
</a>
2130
</td>
2231

2332
<!-- Otherwise, show build name & site on the row. -->
2433
<td ng-if="::cdash.childview != 1" align="left" class="paddt">
2534
<a class="cdash-link" ng-href="sites/{{::build.siteid}}?project={{::cdash.projectid}}&currenttime={{::cdash.unixtimestamp}}">{{::build.site}}</a>
26-
<img ng-if="::build.siteoutoforder == 1" border="0" src="img/flag.png" title="flag"></img>
35+
<span ng-if="::build.siteoutoforder == 1" class="glyphicon glyphicon-flag" style="color: #d9534f;" title="Site out of order"></span>
2736
</td>
2837

2938
<td ng-if="::cdash.childview != 1" align="left">
@@ -52,46 +61,46 @@
5261
name="notesLink"
5362
ng-if="::build.notes > 0"
5463
ng-href="builds/{{::build.id}}/notes">
55-
<img src="img/document.png" alt="Notes" class="icon"/>
64+
<span class="glyphicon glyphicon-file" style="color: #777; margin-right: 4px;"></span>
5665
</a>
5766

5867
<a class="cdash-link"
5968
href="" style="float: left;"
6069
ng-if="::build.uploadfilecount > 0"
6170
ng-href="builds/{{::build.id}}/files"
6271
title="{{::build.uploadfilecount}} files uploaded with this build">
63-
<img src="img/package.png" alt="Files" class="icon"/>
72+
<span class="glyphicon glyphicon-compressed" style="color: #5cb85c; margin-right: 4px;"></span>
6473
</a>
6574

6675
<!-- If the build has errors or test failing -->
6776
<a class="cdash-link"
6877
href="" style="float: left;"
6978
ng-if="::build.compilation.error > 0 || build.test.fail > 0"
7079
ng-click="toggleBuildProblems(build)">
71-
<img src="img/Info.png" alt="info" class="icon"></img>
80+
<span class="glyphicon glyphicon-info-sign" style="color: #777; margin-right: 4px;" title="Build information"></span>
7281
</a>
7382

7483
<!-- If the build is expected and missing -->
7584
<a class="cdash-link"
7685
href="" style="float: left;"
7786
ng-if="::build.expectedandmissing == 1"
7887
ng-click="toggleExpectedInfo(build)">
79-
<img src="img/Info.png" alt="info" class="icon"></img>
88+
<span class="glyphicon glyphicon-info-sign" style="color: #777; margin-right: 4px;" title="Expected build information"></span>
8089
</a>
8190

8291
<!-- Display the note icon -->
8392
<a class="cdash-link" name="Build Notes" id="buildnote_{{::build.id}}"
8493
ng-if="::build.buildnotes > 0"
8594
ng-href="ajax/buildnote.php?buildid={{::build.id}}">
86-
<img src="img/note.png" alt="note" class="icon"></img>
95+
<span class="glyphicon glyphicon-comment" style="color: #337ab7; margin-right: 4px;"></span>
8796
</a>
8897

8998
<div style="float: left;" ng-if="::cdash.user.admin == 1">
90-
<!-- Display folder icon to edit this build for administrative users -->
99+
<!-- Display cog icon to edit this build for administrative users -->
91100
<a class="cdash-link" href="" ng-click="toggleAdminOptions(build)">
92-
<img name="adminoptions" src="img/folder.png" class="icon"/>
101+
<span name="adminoptions" class="glyphicon glyphicon-cog" style="color: #777; margin-right: 4px;" data-cy="build-admin-options"></span>
93102
</a>
94-
<img src="img/loading.gif" ng-if="build.loading == 1"/>
103+
<span class="glyphicon glyphicon-refresh glyphicon-spin" ng-if="build.loading == 1" style="color: #5bc0de;"></span>
95104
</div>
96105
</div>
97106

@@ -168,45 +177,63 @@
168177

169178
<!-- admin options table -->
170179
<div ng-if="::cdash.user.admin == 1">
171-
<div ng-if="build.showAdminOptions == 1">
180+
<div ng-if="build.showAdminOptions == 1" style="margin-top: 8px;">
172181
<table width="100%" border="0" class="animate-show">
173182
<!-- If user is admin of the project propose to group this build -->
174183
<tr ng-repeat="group in ::cdash.all_buildgroups">
175-
<td width="35%">
184+
<td width="35%" style="padding: 4px 0;">
176185
<b>{{::group.name}}</b>:
177186
</td>
178-
<td ng-if="::group.name == buildgroup.name" colspan="2">
179-
<a class="cdash-link" ng-if="build.expected == 0" href="" ng-click="toggleExpected(build, group.id)">
180-
[mark as expected]
181-
</a>
182-
<a class="cdash-link" ng-if="build.expected == 1 || build.expectedandmissing == 1" href="" ng-click="toggleExpected(build, group.id)">
183-
[mark as non expected]
184-
</a>
187+
<td ng-if="::group.name == buildgroup.name" colspan="2" style="padding: 4px 0;">
188+
<button class="btn btn-xs"
189+
ng-class="(build.expected == 1 || build.expectedandmissing == 1) ? 'btn-warning' : 'btn-success'"
190+
ng-click="toggleExpected(build, group.id)"
191+
ng-disabled="build.expectedLoading"
192+
data-cy="{{(build.expected == 1 || build.expectedandmissing == 1) ? 'mark-as-non-expected-btn' : 'mark-as-expected-btn'}}">
193+
<span class="glyphicon" ng-class="{
194+
'glyphicon-ok': !build.expectedLoading && (build.expected != 1 && build.expectedandmissing != 1),
195+
'glyphicon-remove': !build.expectedLoading && (build.expected == 1 || build.expectedandmissing == 1)
196+
}"></span>
197+
<span ng-if="!build.expectedLoading && (build.expected != 1 && build.expectedandmissing != 1)">Mark as Expected</span>
198+
<span ng-if="!build.expectedLoading && (build.expected == 1 || build.expectedandmissing == 1)">Mark as Non Expected</span>
199+
</button>
185200
</td>
186-
<td ng-if="::group.name != buildgroup.name" colspan="2">
187-
<input type="checkbox" ng-model="build.expected" ng-true-value="'1'" ng-false-value="'0'"> expected</input>
201+
<td ng-if="::group.name != buildgroup.name" colspan="2" style="padding: 4px 0;">
202+
<label style="font-weight: normal; margin: 0;">
203+
<input type="checkbox" ng-model="build.expectedInGroup[group.id]" ng-true-value="true" ng-false-value="false"> expected
204+
</label>
188205
</td>
189-
<td ng-if="::group.name != buildgroup.name" class="nob">
190-
<a class="cdash-link" href="" ng-click="moveToGroup(build, group.id)"> [move to group] </a>
206+
<td ng-if="::group.name != buildgroup.name" class="nob" style="padding: 4px 0;">
207+
<button class="btn btn-xs btn-primary" ng-click="moveToGroup(build, group.id)">
208+
<span class="glyphicon glyphicon-arrow-right"></span> Move to Group
209+
</button>
191210
</td>
192211
</tr>
193212
<tr>
194-
<td colspan="3" class="nob">
195-
<a class="cdash-link" href="" ng-click="showModal(build)"> [remove this build] </a>
213+
<td colspan="3" class="nob" style="padding: 8px 0;">
214+
<button class="btn btn-xs btn-danger" ng-click="showModal(build)">
215+
<span class="glyphicon glyphicon-trash"></span> Remove This Build
216+
</button>
196217
</td>
197218
</tr>
198219
</table>
199220

200221
<div tooltip-popup-delay="1500"
201222
tooltip-append-to-body="true"
202223
uib-tooltip="Done builds will be overwritten if a new one is submitted with the same site, build name, and timestamp."
203-
>
204-
<a class="cdash-link" ng-if="build.done == 0" href="" ng-click="toggleDone(build)">
205-
[mark as done]
206-
</a>
207-
<a class="cdash-link" ng-if="build.done == 1" href="" ng-click="toggleDone(build)">
208-
[mark as not done]
209-
</a>
224+
style="margin-top: 4px;"
225+
ng-if="build.id && !build.expectedandmissing">
226+
<button class="btn btn-xs"
227+
ng-class="build.done == 1 ? 'btn-default' : 'btn-info'"
228+
ng-click="toggleDone(build)"
229+
ng-disabled="build.doneLoading">
230+
<span class="glyphicon" ng-class="{
231+
'glyphicon-ok-circle': !build.doneLoading && build.done != 1,
232+
'glyphicon-ban-circle': !build.doneLoading && build.done == 1
233+
}"></span>
234+
<span ng-if="!build.doneLoading && build.done != 1">Mark as Done</span>
235+
<span ng-if="!build.doneLoading && build.done == 1">Mark as Not Done</span>
236+
</button>
210237
</div>
211238
</div>
212239
</div>

0 commit comments

Comments
 (0)