Skip to content
130 changes: 127 additions & 3 deletions resources/js/angular/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function showEmptyBuildsLast() {
};
}

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

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

// Initialize expectedInGroup property for each build to avoid checkbox binding conflicts
for (var j = 0; j < $scope.cdash.buildgroups[i].builds.length; j++) {
$scope.cdash.buildgroups[i].builds[j].expectedInGroup = {};
}

// Initialize bulk selection properties
$scope.cdash.buildgroups[i].selectedBuilds = [];
$scope.cdash.buildgroups[i].selectAll = false;
$scope.cdash.buildgroups[i].bulkTargetGroup = '';
$scope.cdash.buildgroups[i].selectionMode = false;

// Mark this group has having "normal" builds if it only contains missing & expected builds.
if (!$scope.cdash.buildgroups[i].hasnormalbuilds && !$scope.cdash.buildgroups[i].hasparentbuilds && $scope.cdash.buildgroups[i].builds.length > 0) {
$scope.cdash.buildgroups[i].hasnormalbuilds = true;
Expand Down Expand Up @@ -389,7 +400,7 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
};

$scope.showModal = function (buildid) {
modalSvc.showModal(buildid, $scope.removeBuild, 'modal-template');
modalSvc.showModal(buildid, $scope.removeBuild, 'modal-template', null, 'sm', null, null, false);
}

$scope.removeBuild = function(build) {
Expand Down Expand Up @@ -498,20 +509,133 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
$http.post('api/v1/expectedbuild.php', parameters)
.then(function success() {
window.location.reload();
}).catch(function(error) {
console.error('Error moving expected build:', error);
alert('An error occurred while moving the build. Please try again.');
});
} else {
// Use the checkbox value for this specific group, default to current expected value
var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[groupid] !== undefined
? (build.expectedInGroup[groupid] ? 1 : 0)
: build.expected;

// Use the build API with the correct parameters
var parameters = {
buildid: build.id,
newgroupid: groupid,
expected: build.expected
expected: expectedInNewGroup
};
$http.post('api/v1/build.php', parameters)
.then(function success() {
window.location.reload();
}).catch(function(error) {
console.error('Error moving build:', error);
alert('Error moving build: ' + (error.data && error.data.error ? error.data.error : 'Unknown error'));
});
}
};

// Bulk selection functions
$scope.toggleBuildSelection = function(build, buildgroup) {
if (build.selected) {
buildgroup.selectedBuilds.push(build);
} else {
var index = buildgroup.selectedBuilds.indexOf(build);
if (index > -1) {
buildgroup.selectedBuilds.splice(index, 1);
}
buildgroup.selectAll = false;
}
};

$scope.toggleSelectAll = function(buildgroup) {
buildgroup.selectedBuilds = [];
for (var i = 0; i < buildgroup.pagination.filteredBuilds.length; i++) {
var build = buildgroup.pagination.filteredBuilds[i];
if (build.id) { // Only select builds that have IDs (not expected missing builds)
build.selected = buildgroup.selectAll;
if (buildgroup.selectAll) {
buildgroup.selectedBuilds.push(build);
}
}
}
};

$scope.clearBuildSelection = function(buildgroup) {
buildgroup.selectAll = false;
buildgroup.selectedBuilds = [];
for (var i = 0; i < buildgroup.builds.length; i++) {
buildgroup.builds[i].selected = false;
}
};

$scope.toggleSelectionMode = function(buildgroup) {
buildgroup.selectionMode = !buildgroup.selectionMode;
// Clear selection when exiting selection mode
if (!buildgroup.selectionMode) {
$scope.clearBuildSelection(buildgroup);
}
};

$scope.bulkMoveToGroup = function(buildgroup) {
if (!buildgroup.bulkTargetGroup || buildgroup.selectedBuilds.length === 0) {
return;
}

var targetGroupId = parseInt(buildgroup.bulkTargetGroup, 10);
var movePromises = [];

// Move each selected build using the build API
for (var i = 0; i < buildgroup.selectedBuilds.length; i++) {
var build = buildgroup.selectedBuilds[i];
var expectedInNewGroup = build.expectedInGroup && build.expectedInGroup[targetGroupId] !== undefined
? (build.expectedInGroup[targetGroupId] ? 1 : 0)
: (build.expected || 0);

var parameters = {
buildid: build.id,
newgroupid: targetGroupId,
expected: expectedInNewGroup
};
movePromises.push($http.post('api/v1/build.php', parameters));
}

// Wait for all moves to complete, then reload
$q.all(movePromises).then(function() {
window.location.reload();
}).catch(function(error) {
console.error('Error moving builds:', error);
alert('An error occurred while moving builds. Please try again.');
});
};

$scope.bulkMarkAsExpected = function(buildgroup, expectedValue) {
if (buildgroup.selectedBuilds.length === 0) {
return;
}

var updatePromises = [];

// Update expected status for each selected build using the build API
for (var i = 0; i < buildgroup.selectedBuilds.length; i++) {
var build = buildgroup.selectedBuilds[i];
var parameters = {
buildid: build.id,
groupid: parseInt(buildgroup.id, 10),
expected: expectedValue
};
updatePromises.push($http.post('api/v1/build.php', parameters));
}

// Wait for all updates to complete, then reload
$q.all(updatePromises).then(function() {
window.location.reload();
}).catch(function(error) {
console.error('Error updating builds:', error);
alert('An error occurred while updating builds. Please try again.');
});
};

$scope.colorblind_toggle = function() {
if ($scope.cdash.filterdata.colorblind) {
$rootScope.cssfile = "colorblind";
Expand Down
2 changes: 1 addition & 1 deletion resources/js/angular/legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { HeadController } from "./controllers/head";
CDash.controller('HeadController', ["$rootScope", "$document", HeadController]);

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

import { SubProjectController } from "./controllers/subproject";
Expand Down
5 changes: 3 additions & 2 deletions resources/js/angular/services/modal.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export function modalSvc ($uibModal) {
const showModal = function(modelId, okFn, template, parent_scope, size, success, error) {
const showModal = function(modelId, okFn, template, parent_scope, size, success, error, animation) {
parent_scope = typeof parent_scope !== 'undefined' ? parent_scope : null;
size = typeof size !== 'undefined' ? size : 'sm';
animation = typeof animation !== 'undefined' ? animation : true;
var $modal = $uibModal.open({
animation: true,
animation: animation,
backdrop: true,
controller: function () {
var $ctrl = this;
Expand Down
2 changes: 1 addition & 1 deletion resources/js/angular/views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ <h3>
<h4 class="modal-title">Remove Build</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to remove this Build?</p>
<p>Are you sure you want to remove this build?</p>
</div>
<div class="modal-footer">
<button class="btn" ng-click="$ctrl.cancel()">cancel</button>
Expand Down
93 changes: 60 additions & 33 deletions resources/js/angular/views/partials/build.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
<!-- Selection checkbox for bulk actions (admin only) -->
<td ng-if="cdash.user.admin == 1 && buildgroup.selectionMode" align="center" class="paddt">
<input type="checkbox"
ng-if="::build.id"
ng-model="build.selected"
ng-change="toggleBuildSelection(build, buildgroup)"
data-cy="build-selection-checkbox">
</td>

<!-- For child view, display the label(s) as a link to the build summary -->
<td ng-if="::cdash.childview == 1" align="left" class="paddt" colspan="2">
<a class="cdash-link" ng-href="builds/{{::build.id}}">
Expand All @@ -7,7 +16,7 @@
<!-- Icon for build errors / test failing -->
<a class="cdash-link" ng-if="::build.compilation.error > 0 || build.test.fail > 0" href=""
ng-click="toggleBuildProblems(build)">
<img src="img/Info.png" alt="info" class="icon"></img>
<span class="glyphicon glyphicon-info-sign" style="color: #777;" title="Build information"></span>
</a>

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

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

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

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

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

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

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

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

Expand Down Expand Up @@ -168,45 +177,63 @@

<!-- admin options table -->
<div ng-if="::cdash.user.admin == 1">
<div ng-if="build.showAdminOptions == 1">
<div ng-if="build.showAdminOptions == 1" style="margin-top: 8px;">
<table width="100%" border="0" class="animate-show">
<!-- If user is admin of the project propose to group this build -->
<tr ng-repeat="group in ::cdash.all_buildgroups">
<td width="35%">
<td width="35%" style="padding: 4px 0;">
<b>{{::group.name}}</b>:
</td>
<td ng-if="::group.name == buildgroup.name" colspan="2">
<a class="cdash-link" ng-if="build.expected == 0" href="" ng-click="toggleExpected(build, group.id)">
[mark as expected]
</a>
<a class="cdash-link" ng-if="build.expected == 1 || build.expectedandmissing == 1" href="" ng-click="toggleExpected(build, group.id)">
[mark as non expected]
</a>
<td ng-if="::group.name == buildgroup.name" colspan="2" style="padding: 4px 0;">
<button class="btn btn-xs"
ng-class="(build.expected == 1 || build.expectedandmissing == 1) ? 'btn-warning' : 'btn-success'"
ng-click="toggleExpected(build, group.id)"
ng-disabled="build.expectedLoading"
data-cy="{{(build.expected == 1 || build.expectedandmissing == 1) ? 'mark-as-non-expected-btn' : 'mark-as-expected-btn'}}">
<span class="glyphicon" ng-class="{
'glyphicon-ok': !build.expectedLoading && (build.expected != 1 && build.expectedandmissing != 1),
'glyphicon-remove': !build.expectedLoading && (build.expected == 1 || build.expectedandmissing == 1)
}"></span>
<span ng-if="!build.expectedLoading && (build.expected != 1 && build.expectedandmissing != 1)">Mark as Expected</span>
<span ng-if="!build.expectedLoading && (build.expected == 1 || build.expectedandmissing == 1)">Mark as Non Expected</span>
</button>
</td>
<td ng-if="::group.name != buildgroup.name" colspan="2">
<input type="checkbox" ng-model="build.expected" ng-true-value="'1'" ng-false-value="'0'"> expected</input>
<td ng-if="::group.name != buildgroup.name" colspan="2" style="padding: 4px 0;">
<label style="font-weight: normal; margin: 0;">
<input type="checkbox" ng-model="build.expectedInGroup[group.id]" ng-true-value="true" ng-false-value="false"> expected
</label>
</td>
<td ng-if="::group.name != buildgroup.name" class="nob">
<a class="cdash-link" href="" ng-click="moveToGroup(build, group.id)"> [move to group] </a>
<td ng-if="::group.name != buildgroup.name" class="nob" style="padding: 4px 0;">
<button class="btn btn-xs btn-primary" ng-click="moveToGroup(build, group.id)">
<span class="glyphicon glyphicon-arrow-right"></span> Move to Group
</button>
</td>
</tr>
<tr>
<td colspan="3" class="nob">
<a class="cdash-link" href="" ng-click="showModal(build)"> [remove this build] </a>
<td colspan="3" class="nob" style="padding: 8px 0;">
<button class="btn btn-xs btn-danger" ng-click="showModal(build)">
<span class="glyphicon glyphicon-trash"></span> Remove This Build
</button>
</td>
</tr>
</table>

<div tooltip-popup-delay="1500"
tooltip-append-to-body="true"
uib-tooltip="Done builds will be overwritten if a new one is submitted with the same site, build name, and timestamp."
>
<a class="cdash-link" ng-if="build.done == 0" href="" ng-click="toggleDone(build)">
[mark as done]
</a>
<a class="cdash-link" ng-if="build.done == 1" href="" ng-click="toggleDone(build)">
[mark as not done]
</a>
style="margin-top: 4px;"
ng-if="build.id && !build.expectedandmissing">
<button class="btn btn-xs"
ng-class="build.done == 1 ? 'btn-default' : 'btn-info'"
ng-click="toggleDone(build)"
ng-disabled="build.doneLoading">
<span class="glyphicon" ng-class="{
'glyphicon-ok-circle': !build.doneLoading && build.done != 1,
'glyphicon-ban-circle': !build.doneLoading && build.done == 1
}"></span>
<span ng-if="!build.doneLoading && build.done != 1">Mark as Done</span>
<span ng-if="!build.doneLoading && build.done == 1">Mark as Not Done</span>
</button>
</div>
</div>
</div>
Expand Down
Loading