Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions resources/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ body {
text-decoration: underline;
}

/* Bootstrap forces a focus ring on every a:focus, which shows up on mouse
clicks too. Suppress it only when the browser wouldn't draw one itself, so
keyboard navigation keeps a visible focus indicator. */
a.cdash-link:focus:not(:focus-visible) {
outline: none;
}

.na { background-color : #cccccc; }
.measurement { background-color : #b0c4de; }

Expand Down
30 changes: 15 additions & 15 deletions resources/js/angular/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ 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
// Initialize per-build admin UI state (default move target = current group)
for (var j = 0; j < $scope.cdash.buildgroups[i].builds.length; j++) {
$scope.cdash.buildgroups[i].builds[j].expectedInGroup = {};
$scope.cdash.buildgroups[i].builds[j].moveTargetGroup = String($scope.cdash.buildgroups[i].id);
}

// Initialize bulk selection properties
Expand Down Expand Up @@ -491,6 +491,15 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
};

$scope.moveToGroup = function(build, groupid) {
if (!groupid) {
return;
}

groupid = parseInt(groupid, 10);
if (!groupid || groupid === parseInt(build.buildgroupid, 10)) {
return;
}

if (build.expectedandmissing == 1) {
var parameters = {
siteid: build.siteid,
Expand All @@ -507,16 +516,11 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
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
// Preserve the build's current expected status when moving groups.
var parameters = {
buildid: build.id,
newgroupid: groupid,
expected: expectedInNewGroup
expected: build.expected || 0
};
$http.post('api/v1/build.php', parameters)
.then(function success() {
Expand Down Expand Up @@ -578,17 +582,13 @@ export function IndexController($scope, $rootScope, $location, $http, $filter, $
var targetGroupId = parseInt(buildgroup.bulkTargetGroup, 10);
var movePromises = [];

// Move each selected build using the build API
// Move each selected build using the build API, preserving expected status
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
expected: build.expected || 0
};
movePromises.push($http.post('api/v1/build.php', parameters));
}
Expand Down
106 changes: 51 additions & 55 deletions resources/js/angular/views/partials/build.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,64 +175,60 @@
</table>
</div>

<!-- admin options table -->
<div ng-if="::cdash.user.admin == 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%" style="padding: 4px 0;">
<b>{{::group.name}}</b>:
</td>
<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" 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" 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" 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."
style="margin-top: 4px;"
ng-if="build.id && !build.expectedandmissing">
<!-- admin options (cleared below the build name / icon row) -->
<div ng-if="::cdash.user.admin == 1" style="clear: both;">
<div ng-if="build.showAdminOptions == 1"
class="animate-show"
data-cy="build-admin-options-panel"
style="padding: 8px 0; display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
<button class="btn btn-xs"
ng-class="build.done == 1 ? 'btn-default' : 'btn-info'"
ng-click="toggleDone(build)"
ng-disabled="build.doneLoading">
ng-class="(build.expected == 1 || build.expectedandmissing == 1) ? 'btn-warning' : 'btn-success'"
ng-click="toggleExpected(build, buildgroup.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-circle': !build.doneLoading && build.done != 1,
'glyphicon-ban-circle': !build.doneLoading && build.done == 1
'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.doneLoading && build.done != 1">Mark as Done</span>
<span ng-if="!build.doneLoading && build.done == 1">Mark as Not Done</span>
<span ng-if="!build.expectedLoading && (build.expected != 1 && build.expectedandmissing != 1)">Mark this build as expected</span>
<span ng-if="!build.expectedLoading && (build.expected == 1 || build.expectedandmissing == 1)">Mark this build as not expected</span>
</button>

<button class="btn btn-xs btn-danger" ng-click="showModal(build)">
<span class="glyphicon glyphicon-trash"></span> Remove This Build
</button>

<span ng-if="build.id && !build.expectedandmissing"
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.">
<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>
</span>
</div>

<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px;">
<select ng-model="build.moveTargetGroup"
style="padding: 2px 6px;"
data-cy="move-to-group-select">
<option ng-repeat="group in ::cdash.all_buildgroups"
value="{{group.id}}">{{group.name}}</option>
</select>
<button class="btn btn-xs btn-primary"
ng-click="moveToGroup(build, build.moveTargetGroup)"
ng-disabled="!build.moveTargetGroup || build.moveTargetGroup == buildgroup.id"
data-cy="move-to-group-btn">
<span class="glyphicon glyphicon-arrow-right"></span> Move to group
</button>
</div>
</div>
Expand Down
109 changes: 98 additions & 11 deletions tests/cypress/e2e/expected-build.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ describe('expected_build', () => {
cy.login();
cy.visit('index.php?project=InsightExample&date=2018-08-09');
cy.get('[data-cy="build-admin-options"]').first().click();
cy.get('table.animate-show').find('tr').eq(2).then((row) => {
if (row.find('[data-cy="mark-as-non-expected-btn"]').length > 0) {
row.find('button').click();
cy.get('[data-cy="build-admin-options-panel"]').then(($panel) => {
const btn = $panel.find('[data-cy="mark-as-non-expected-btn"]');
if (btn.length > 0) {
cy.wrap(btn).click();
}
});
});
Expand All @@ -24,14 +25,18 @@ describe('expected_build', () => {
cy.get('@build_td').should('contain', 'test-build-relationships');
cy.get('@build_td').find('[data-cy="build-admin-options"]').click();

// find the 'Mark as Expected' button and click it
cy.get('[data-cy="mark-as-expected-btn"]').click();
// status should start as not expected
cy.get('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-expected-btn"]')
.should('contain', 'Mark this build as expected')
.click();

// refresh the page to make sure this build is now expected
cy.reload();
cy.get('[data-cy="build-admin-options"]').first().click();
cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist');
cy.get('[data-cy="mark-as-non-expected-btn"]').should('exist');
cy.get('[data-cy="mark-as-non-expected-btn"]').should('contain', 'Mark this build as not expected');

// 'latest' should now display 'test-build-relationships' with unknown start time
cy.get('a').contains('Latest').click();
Expand All @@ -50,6 +55,86 @@ describe('expected_build', () => {
cy.get('[data-cy="mark-as-expected-btn"]').should('exist');
});

it('defaults move dropdown to the current group and enables Move only for a different group', () => {
cy.visit('index.php?project=InsightExample&date=2018-08-09');

cy.get('#project_5_15').parents('.buildgroup').first().as('buildgroup');
cy.get('@buildgroup').find('a.grouptrigger').invoke('text').then((currentGroupName) => {
const groupName = currentGroupName.trim();

cy.get('#project_5_15').find('tbody').find('tr').first().find('td').eq(1).as('build_td');
cy.get('@build_td').find('[data-cy="build-admin-options"]').click();

cy.get('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-expected-btn"]').should('contain', 'Mark this build as expected');
cy.get('[data-cy="move-to-group-btn"]').should('contain', 'Move to group').and('be.disabled');
cy.contains('-- Select Group --').should('not.exist');

cy.get('[data-cy="move-to-group-select"]').find('option').should('have.length.at.least', 2);
cy.get('[data-cy="move-to-group-select"] option:selected')
.should('contain', groupName);

cy.get('[data-cy="move-to-group-select"]').find('option').then(($options) => {
const other = [...$options].find((o) => o.textContent.trim() !== groupName);
expect(other).to.exist;
cy.get('[data-cy="move-to-group-select"]').select(other.value);
});
cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled');

cy.get('[data-cy="move-to-group-select"]').select(groupName);
cy.get('[data-cy="move-to-group-btn"]').should('be.disabled');
});
});

it('moves a build to another group via dropdown', () => {
cy.visit('index.php?project=InsightExample&date=2018-08-09');

const buildName = 'test-build-relationships';
cy.get('#project_5_15').parents('.buildgroup').first().as('source_group');
cy.get('@source_group').find('a.grouptrigger').invoke('text').then((sourceGroupName) => {
const sourceName = sourceGroupName.trim();

cy.get('#project_5_15').find('tbody').find('tr').first().find('td').eq(1).as('build_td');
cy.get('@build_td').should('contain', buildName);
cy.get('@build_td').find('[data-cy="build-admin-options"]').click();

cy.get('[data-cy="move-to-group-select"]').find('option').then(($options) => {
const other = [...$options].find((o) => o.textContent.trim() !== sourceName);
expect(other).to.exist;
const targetGroupId = other.value;
const targetGroupName = other.textContent.trim();

cy.window().then((w) => {
w.beforeMoveReload = true;
});
cy.get('[data-cy="move-to-group-select"]').select(targetGroupId);
cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled').click();

cy.window().should('not.have.property', 'beforeMoveReload');
cy.url().should('contain', 'index.php?project=InsightExample&date=2018-08-09');

// build should appear in the destination group
cy.contains('.buildgroup', targetGroupName).should('contain', buildName);

// move it back to the original group
cy.contains('.buildgroup', targetGroupName).within(() => {
cy.contains('tr', buildName).find('[data-cy="build-admin-options"]').click();
cy.get('[data-cy="move-to-group-select"] option:selected')
.should('contain', targetGroupName);
cy.get('[data-cy="move-to-group-btn"]').should('be.disabled');
cy.get('[data-cy="move-to-group-select"]').select(sourceName);
cy.window().then((w) => {
w.beforeMoveBackReload = true;
});
cy.get('[data-cy="move-to-group-btn"]').should('not.be.disabled').click();
});

cy.window().should('not.have.property', 'beforeMoveBackReload');
cy.contains('.buildgroup', sourceName).should('contain', buildName);
});
});
});

it('batch marks multiple builds as expected and not expected', () => {
// navigate to the page with builds
cy.visit('index.php?project=InsightExample&date=2010-07-07');
Expand All @@ -76,15 +161,16 @@ describe('expected_build', () => {

// verify first build is now expected
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click();
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('table.animate-show').should('be.visible');
cy.get('[data-cy="mark-as-non-expected-btn"]').first().should('exist');
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-non-expected-btn"]').first().should('exist')
.and('contain', 'Mark this build as not expected');
cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist');
// close admin options
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click();

// Check second build
cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options"]').click();
cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('table.animate-show').should('be.visible');
cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-non-expected-btn"]').should('exist');
cy.get('[data-cy="mark-as-expected-btn"]').should('not.exist');
// close admin options
Expand All @@ -107,14 +193,15 @@ describe('expected_build', () => {

// verify first build is now not expected
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click();
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('table.animate-show').should('be.visible');
cy.get('[data-cy="mark-as-expected-btn"]').first().should('exist');
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-expected-btn"]').first().should('exist')
.and('contain', 'Mark this build as expected');
cy.get('[data-cy="mark-as-non-expected-btn"]').should('not.exist');
// close admin options
cy.get('#project_5_13').find('tbody').find('tr').eq(0).find('[data-cy="build-admin-options"]').click();

cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options"]').click();
cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('table.animate-show').should('be.visible');
cy.get('#project_5_13').find('tbody').find('tr').eq(1).find('[data-cy="build-admin-options-panel"]').should('be.visible');
cy.get('[data-cy="mark-as-expected-btn"]').should('exist');
cy.get('[data-cy="mark-as-non-expected-btn"]').should('not.exist');
// close admin options
Expand Down