diff --git a/resources/css/common.css b/resources/css/common.css
index d6a7ceffce..1b8798106d 100644
--- a/resources/css/common.css
+++ b/resources/css/common.css
@@ -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; }
diff --git a/resources/js/angular/controllers/index.js b/resources/js/angular/controllers/index.js
index 50b99d19a1..020aac9b72 100644
--- a/resources/js/angular/controllers/index.js
+++ b/resources/js/angular/controllers/index.js
@@ -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
@@ -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,
@@ -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() {
@@ -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));
}
diff --git a/resources/js/angular/views/partials/build.html b/resources/js/angular/views/partials/build.html
index ed5b8dc1a9..4d7e2de8a4 100644
--- a/resources/js/angular/views/partials/build.html
+++ b/resources/js/angular/views/partials/build.html
@@ -175,64 +175,60 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/cypress/e2e/expected-build.cy.js b/tests/cypress/e2e/expected-build.cy.js
index cd13343cf1..a582660bc9 100644
--- a/tests/cypress/e2e/expected-build.cy.js
+++ b/tests/cypress/e2e/expected-build.cy.js
@@ -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();
}
});
});
@@ -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();
@@ -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');
@@ -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
@@ -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