From bd70d5429528408c96b805f54cc4a9ecc24f0570 Mon Sep 17 00:00:00 2001 From: Shivam K Date: Mon, 15 Jun 2026 12:59:04 +0200 Subject: [PATCH 1/9] fix: Experiment table sorting not working --- .zed/debug.json | 23 +++++++++++++++++++ apps/backend/package.json | 2 +- .../postgres/ExperimentDataSource.ts | 9 ++++++++ .../experiment/ExperimentsTable.tsx | 2 ++ package.json | 1 + 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .zed/debug.json diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000000..bcfa80b72b --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,23 @@ +[ + { + "label": "Launch Debug React in Chrome", + "adapter": "JavaScript", + "type": "chrome", + "request": "launch", + "console": "integratedTerminal", + "url": "http://localhost:3000", + "webRoot": "$ZED_WORKTREE_ROOT/apps/frontend", + "skipFiles": ["/**"], + }, + { + "label": "Debug Local Backend {attach} (npm run dev)", + "adapter": "JavaScript", + "type": "node", + "request": "attach", + "port": 9229, + "address": "localhost", + "restart": true, + "localRoot": "$ZED_WORKTREE_ROOT/apps/backend", + "skipFiles": ["/**"], + }, +] diff --git a/apps/backend/package.json b/apps/backend/package.json index cf652a373d..b12fddc936 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -19,7 +19,7 @@ "build:validation": "npm --prefix ../../validation ci && npm --prefix ../../validation run build", "build": "npm run build:validation && npm run generate:uows && rm -rf ./build && tsc", "postinstall": "npm run build:validation", - "dev": "docker compose up -d --remove-orphans && ts-node-dev -r tsconfig-paths/register --respawn -T ./index.ts", + "dev": "docker compose up -d --remove-orphans && ts-node-dev --inspect=9229 -r tsconfig-paths/register --respawn -T ./index.ts", "start:e2e": "docker compose -f docker-compose.e2e.yml up -d --remove-orphans && wait-on --verbose --interval 5000 --delay 15000 --timeout 300000 tcp:5432 && node ./build/index.js", "start:e2e:stfc": "docker compose -f docker-compose-stfc.e2e.yml up -d --remove-orphans && wait-on --verbose --interval 5000 --delay 15000 --timeout 300000 tcp:5432 && node ./build/index.js", "dev:docker": "npm install && ts-node-dev --inspect=0.0.0.0:9229 --respawn -T ./index.ts", diff --git a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts index 5496aae7af..58d42ff0ae 100644 --- a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts @@ -83,8 +83,17 @@ function generateExperimentId( return `${proposalNumber}-${sequence ?? 0}`; } +// Which fields we are allowed to sort on const fieldMap: { [key: string]: string } = { + // These fields are in ExperimentsTable.tsx (frontend), + // refer Column[] and field experimentId: 'experiment_id', + 'proposal.proposalId': 'proposal_id', + startsAt: 'starts_at', + endsAt: 'ends_at', + // We don't sort on the fields below, create a view later if sorting needed + // 'instrument.name': 'instrument_id', + // 'experimentSafety.status.name': '', }; @injectable() diff --git a/apps/frontend/src/components/experiment/ExperimentsTable.tsx b/apps/frontend/src/components/experiment/ExperimentsTable.tsx index fbe981241f..bb90f38eab 100644 --- a/apps/frontend/src/components/experiment/ExperimentsTable.tsx +++ b/apps/frontend/src/components/experiment/ExperimentsTable.tsx @@ -182,10 +182,12 @@ export default function ExperimentsTable({ { title: 'Instrument', field: 'instrument.name', + sorting: false, }, { title: 'Experiment Safety Status', field: 'experimentSafety.status.name', + sorting: false, render: (rowData: Experiment) => rowData.experimentSafety?.status?.name ?? 'ESF Not Started', }, diff --git a/package.json b/package.json index 6819b06952..1da7555fa0 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ ], "dependencies": { "concurrently": "^9.0.1", + "ts-node-dev": "^2.0.0", "wait-on": "^9.0.1" }, "devDependencies": { From 78eaffcf8de919e98e08ef7f35eb23d91693e3c9 Mon Sep 17 00:00:00 2001 From: Shivam K Date: Mon, 15 Jun 2026 14:38:56 +0200 Subject: [PATCH 2/9] chore: add e2e tests for experiment table sorting --- apps/e2e/cypress/e2e/experiments.cy.ts | 624 +++++++++++++------------ 1 file changed, 328 insertions(+), 296 deletions(-) diff --git a/apps/e2e/cypress/e2e/experiments.cy.ts b/apps/e2e/cypress/e2e/experiments.cy.ts index 4d81b90118..0dcd21296a 100644 --- a/apps/e2e/cypress/e2e/experiments.cy.ts +++ b/apps/e2e/cypress/e2e/experiments.cy.ts @@ -1,296 +1,328 @@ -import { - FeatureId, - ProposalEndStatus, -} from '@user-office-software-libs/shared-types'; - -import featureFlags from '../support/featureFlags'; -import initialDBData from '../support/initialDBData'; - -context('Experiments tests', () => { - const instrumentScientist1 = initialDBData.users.instrumentScientist1; - - beforeEach(function () { - cy.resetDB(true); - cy.getAndStoreFeaturesEnabled().then(() => { - // NOTE: We can check features after they are stored to the local storage - if ( - !featureFlags.getEnabledFeatures().get(FeatureId.SCHEDULER) || - !featureFlags - .getEnabledFeatures() - .get(FeatureId.EXPERIMENT_SAFETY_REVIEW) - ) { - this.skip(); - } - }); - - cy.viewport(1920, 1080); - - cy.updateProposalManagementDecision({ - proposalPk: initialDBData.proposal.id, - finalStatus: ProposalEndStatus.ACCEPTED, - managementTimeAllocations: [ - { instrumentId: initialDBData.instrument1.id, value: 5 }, - ], - managementDecisionSubmitted: true, - }); - cy.createExperimentSafety({ - experimentPk: initialDBData.experiments.upcoming.experimentPk, - }); - cy.createVisit({ - experimentPk: initialDBData.experiments.upcoming.experimentPk, - team: [ - initialDBData.users.user1.id, - initialDBData.users.user2.id, - initialDBData.users.user3.id, - ], - teamLeadUserId: initialDBData.users.user1.id, - }); - }); - - describe('Experiments tests', () => { - it('Can filter by call and instrument', () => { - cy.login('officer'); - cy.visit('/'); - cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); - cy.finishedLoading(); - cy.get('button[value=NONE]').click(); - - cy.get('[data-cy=call-filter]').click(); - cy.get('[role=presentation]').contains('call 1').click(); - cy.contains('1-4 of 4'); - - cy.get('[data-cy=instrument-filter]').click(); - cy.get('[role=presentation]').contains('Instrument 3').click(); - cy.contains('0-0 of 0'); - - cy.get('[data-cy=instrument-filter]').click(); - cy.get('[role=presentation]').contains('Instrument 2').click(); - cy.contains('1-2 of 2'); - - cy.get('[data-cy=instrument-filter]').click(); - cy.get('[role=presentation]').contains('Instrument 1').click(); - cy.contains('1-2 of 2'); - }); - - it('Can filter by date', () => { - cy.login('officer'); - cy.visit('/'); - cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); - - cy.get('[value=TODAY]').click(); - cy.contains('0-0 of 0'); - - cy.get('[value=NONE]').click(); - cy.contains('1-4 of 4'); - }); - - it('Can view visits', () => { - cy.login('officer'); - cy.visit('/'); - cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); - cy.get('[value=NONE]').click(); - - cy.finishedLoading(); - - cy.get('[data-cy=officer-scheduled-events-table] Table button') - .first() - .click(); - cy.get('button[role="tab"]').contains('Visit').click({ force: true }); - cy.contains(initialDBData.users.user1.lastName); - }); - - it('All the columns in visit table are sortable', () => { - cy.login('officer'); - cy.visit('/'); - cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); - cy.get('[value=NONE]').click(); - - cy.finishedLoading(); - - cy.get('[data-cy=officer-scheduled-events-table] Table button') - .first() - .click(); - cy.get('button[role="tab"]').contains('Visit').click({ force: true }); - let tableValue: string[] = []; - cy.get('[data-cy=visit-registrations-table] tbody td') - .each(($el) => { - tableValue = [...tableValue, $el.text().toString()]; - }) - .then(() => { - // Explanation: The table has 7 columns. We will sort each column in ascending and descending order and check if the table is sorted correctly. - // tableColumns: Array of objects. Each object contains the title of the column and the data of the column in original, ascending and descending order. - const tableColumns = [ - { - title: 'Actions', - data: { - original: tableValue.filter((d, i) => i % 6 === 0), - asc: tableValue.filter((d, i) => i % 6 === 0), - desc: tableValue.filter((d, i) => i % 6 === 0), - }, - }, - { - title: 'Status', - data: { - original: tableValue.filter((d, i) => i % 6 === 1), - asc: tableValue.filter((d, i) => i % 6 === 1).sort(), - desc: tableValue - .filter((d, i) => i % 6 === 1) - .sort() - .reverse(), - }, - }, - { - title: 'Visitor name', - data: { - original: tableValue.filter((d, i) => i % 6 === 2), - asc: tableValue.filter((d, i) => i % 6 === 2).sort(), - desc: tableValue - .filter((d, i) => i % 6 === 2) - .sort() - .reverse(), - }, - }, - { - title: 'Teamleader', - data: { - original: tableValue.filter((d, i) => i % 6 === 3), - asc: tableValue.filter((d, i) => i % 6 === 3).sort(), - desc: tableValue - .filter((d, i) => i % 6 === 3) - .sort() - .reverse(), - }, - }, - { - title: 'Visit start', - data: { - original: tableValue.filter((d, i) => i % 6 === 4), - asc: tableValue.filter((d, i) => i % 6 === 4).sort(), - desc: tableValue - .filter((d, i) => i % 6 === 4) - .sort() - .reverse(), - }, - }, - { - title: 'Visit end', - data: { - original: tableValue.filter((d, i) => i % 6 === 5), - asc: tableValue.filter((d, i) => i % 6 === 5).sort(), - desc: tableValue - .filter((d, i) => i % 6 === 5) - .sort() - .reverse(), - }, - }, - ]; - - // Sort each column in ascending and descending order and check if the table is sorted correctly. - for (let i = 0; i < tableColumns.length; i++) { - cy.get('[data-cy=visit-registrations-table] thead th') - .contains(tableColumns[i].title) - .click(); - - // Check if the table is sorted in ascending order - cy.get('[data-cy=visit-registrations-table] tbody td').each( - ($el, index) => { - if (index % 6 === i) { - expect($el.text()).to.eq( - tableColumns[i].data.asc[Math.floor(index / 6)] - ); - } - } - ); - - // Check if the table is sorted in descending order - cy.get('[data-cy=visit-registrations-table] thead th') - .contains(tableColumns[i].title) - .click(); - - cy.get('[data-cy=visit-registrations-table] tbody td').each( - ($el, index) => { - if (index % 6 === i) { - expect($el.text()).to.eq( - tableColumns[i].data.desc[Math.floor(index / 6)] - ); - } - } - ); - - // Check if the table is sorted in original order - cy.get('[data-cy=visit-registrations-table] thead th') - .contains(tableColumns[i].title) - .click(); - - cy.get('[data-cy=visit-registrations-table] tbody td').each( - ($el, index) => { - if (index % 6 === i) { - expect($el.text()).to.eq( - tableColumns[i].data.original[Math.floor(index / 6)] - ); - } - } - ); - - // Reset the table to original order. How? Click on Hide button first and Expand button then. - cy.reload(); - } - }); - }); - - it('Instrument Scientists should be able to see Experiments only associated with their instruments', () => { - cy.login(instrumentScientist1); - cy.visit('/experiments'); - cy.finishedLoading(); - - // There should be a div with data-cy experiments-table, which will contain table inside it - cy.get('[data-cy=experiments-table]').should('exist'); - - // Wait for the table to load with data - cy.get('[data-cy=experiments-table] table tbody tr').should( - 'have.length.at.least', - 1 - ); - - // Inside the table, there should be a column with title "Instrument" - cy.get('[data-cy=experiments-table] table thead th') - .contains('Instrument') - .should('exist'); - - // This column should only contains with value Instrument 1. Make sure you check only against that column and not others - // Make sure the table is fully loaded by checking that data exists - cy.get('[data-cy=experiments-table] table tbody tr td').should('exist'); - - // Get the header row and find the Instrument column index - cy.get('[data-cy=experiments-table] table thead tr th').then( - ($headers) => { - const headers = Array.from($headers).map((header) => - Cypress.$(header).text().trim() - ); - const instrumentColumnIndex = headers.findIndex( - (header) => header === 'Instrument' - ); - - cy.log(`Found headers: ${JSON.stringify(headers)}`); - cy.log(`Instrument column index: ${instrumentColumnIndex}`); - - // Verify we found the column - expect(instrumentColumnIndex).to.be.greaterThan(-1); - - // Now check all rows in the table body for this specific column - // Use a more robust approach that re-queries the DOM each time - cy.get('[data-cy=experiments-table] table tbody tr').then(($rows) => { - const rowCount = $rows.length; - for (let i = 0; i < rowCount; i++) { - cy.get('[data-cy=experiments-table] table tbody tr') - .eq(i) - .find('td') - .eq(instrumentColumnIndex) - .should('contain.text', 'Instrument 1'); - } - }); - } - ); - }); - }); -}); +import { + FeatureId, + ProposalEndStatus, +} from '@user-office-software-libs/shared-types'; + +import featureFlags from '../support/featureFlags'; +import initialDBData from '../support/initialDBData'; + +context('Experiments tests', () => { + const instrumentScientist1 = initialDBData.users.instrumentScientist1; + + beforeEach(function () { + cy.resetDB(true); + cy.getAndStoreFeaturesEnabled().then(() => { + // NOTE: We can check features after they are stored to the local storage + if ( + !featureFlags.getEnabledFeatures().get(FeatureId.SCHEDULER) || + !featureFlags + .getEnabledFeatures() + .get(FeatureId.EXPERIMENT_SAFETY_REVIEW) + ) { + this.skip(); + } + }); + + cy.viewport(1920, 1080); + + cy.updateProposalManagementDecision({ + proposalPk: initialDBData.proposal.id, + finalStatus: ProposalEndStatus.ACCEPTED, + managementTimeAllocations: [ + { instrumentId: initialDBData.instrument1.id, value: 5 }, + ], + managementDecisionSubmitted: true, + }); + cy.createExperimentSafety({ + experimentPk: initialDBData.experiments.upcoming.experimentPk, + }); + cy.createVisit({ + experimentPk: initialDBData.experiments.upcoming.experimentPk, + team: [ + initialDBData.users.user1.id, + initialDBData.users.user2.id, + initialDBData.users.user3.id, + ], + teamLeadUserId: initialDBData.users.user1.id, + }); + }); + + describe('Experiments tests', () => { + it('Can filter by call and instrument', () => { + cy.login('officer'); + cy.visit('/'); + cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); + cy.finishedLoading(); + cy.get('button[value=NONE]').click(); + + cy.get('[data-cy=call-filter]').click(); + cy.get('[role=presentation]').contains('call 1').click(); + cy.contains('1-4 of 4'); + + cy.get('[data-cy=instrument-filter]').click(); + cy.get('[role=presentation]').contains('Instrument 3').click(); + cy.contains('0-0 of 0'); + + cy.get('[data-cy=instrument-filter]').click(); + cy.get('[role=presentation]').contains('Instrument 2').click(); + cy.contains('1-2 of 2'); + + cy.get('[data-cy=instrument-filter]').click(); + cy.get('[role=presentation]').contains('Instrument 1').click(); + cy.contains('1-2 of 2'); + }); + + it('Can filter by date', () => { + cy.login('officer'); + cy.visit('/'); + cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); + + cy.get('[value=TODAY]').click(); + cy.contains('0-0 of 0'); + + cy.get('[value=NONE]').click(); + cy.contains('1-4 of 4'); + }); + + it('Columns in Experiments Table are sortable', () => { + cy.login('officer'); + cy.visit('/'); + + cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); + cy.finishedLoading(); + + cy.get( + 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + }); + + it('Can view visits', () => { + cy.login('officer'); + cy.visit('/'); + cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); + cy.get('[value=NONE]').click(); + + cy.finishedLoading(); + + cy.get('[data-cy=officer-scheduled-events-table] Table button') + .first() + .click(); + cy.get('button[role="tab"]').contains('Visit').click({ force: true }); + cy.contains(initialDBData.users.user1.lastName); + }); + + it('All the columns in visit table are sortable', () => { + cy.login('officer'); + cy.visit('/'); + cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); + cy.get('[value=NONE]').click(); + + cy.finishedLoading(); + + cy.get('[data-cy=officer-scheduled-events-table] Table button') + .first() + .click(); + cy.get('button[role="tab"]').contains('Visit').click({ force: true }); + let tableValue: string[] = []; + cy.get('[data-cy=visit-registrations-table] tbody td') + .each(($el) => { + tableValue = [...tableValue, $el.text().toString()]; + }) + .then(() => { + // Explanation: The table has 7 columns. We will sort each column in ascending and descending order and check if the table is sorted correctly. + // tableColumns: Array of objects. Each object contains the title of the column and the data of the column in original, ascending and descending order. + const tableColumns = [ + { + title: 'Actions', + data: { + original: tableValue.filter((d, i) => i % 6 === 0), + asc: tableValue.filter((d, i) => i % 6 === 0), + desc: tableValue.filter((d, i) => i % 6 === 0), + }, + }, + { + title: 'Status', + data: { + original: tableValue.filter((d, i) => i % 6 === 1), + asc: tableValue.filter((d, i) => i % 6 === 1).sort(), + desc: tableValue + .filter((d, i) => i % 6 === 1) + .sort() + .reverse(), + }, + }, + { + title: 'Visitor name', + data: { + original: tableValue.filter((d, i) => i % 6 === 2), + asc: tableValue.filter((d, i) => i % 6 === 2).sort(), + desc: tableValue + .filter((d, i) => i % 6 === 2) + .sort() + .reverse(), + }, + }, + { + title: 'Teamleader', + data: { + original: tableValue.filter((d, i) => i % 6 === 3), + asc: tableValue.filter((d, i) => i % 6 === 3).sort(), + desc: tableValue + .filter((d, i) => i % 6 === 3) + .sort() + .reverse(), + }, + }, + { + title: 'Visit start', + data: { + original: tableValue.filter((d, i) => i % 6 === 4), + asc: tableValue.filter((d, i) => i % 6 === 4).sort(), + desc: tableValue + .filter((d, i) => i % 6 === 4) + .sort() + .reverse(), + }, + }, + { + title: 'Visit end', + data: { + original: tableValue.filter((d, i) => i % 6 === 5), + asc: tableValue.filter((d, i) => i % 6 === 5).sort(), + desc: tableValue + .filter((d, i) => i % 6 === 5) + .sort() + .reverse(), + }, + }, + ]; + + // Sort each column in ascending and descending order and check if the table is sorted correctly. + for (let i = 0; i < tableColumns.length; i++) { + cy.get('[data-cy=visit-registrations-table] thead th') + .contains(tableColumns[i].title) + .click(); + + // Check if the table is sorted in ascending order + cy.get('[data-cy=visit-registrations-table] tbody td').each( + ($el, index) => { + if (index % 6 === i) { + expect($el.text()).to.eq( + tableColumns[i].data.asc[Math.floor(index / 6)] + ); + } + } + ); + + // Check if the table is sorted in descending order + cy.get('[data-cy=visit-registrations-table] thead th') + .contains(tableColumns[i].title) + .click(); + + cy.get('[data-cy=visit-registrations-table] tbody td').each( + ($el, index) => { + if (index % 6 === i) { + expect($el.text()).to.eq( + tableColumns[i].data.desc[Math.floor(index / 6)] + ); + } + } + ); + + // Check if the table is sorted in original order + cy.get('[data-cy=visit-registrations-table] thead th') + .contains(tableColumns[i].title) + .click(); + + cy.get('[data-cy=visit-registrations-table] tbody td').each( + ($el, index) => { + if (index % 6 === i) { + expect($el.text()).to.eq( + tableColumns[i].data.original[Math.floor(index / 6)] + ); + } + } + ); + + // Reset the table to original order. How? Click on Hide button first and Expand button then. + cy.reload(); + } + }); + }); + + it('Instrument Scientists should be able to see Experiments only associated with their instruments', () => { + cy.login(instrumentScientist1); + cy.visit('/experiments'); + cy.finishedLoading(); + + // There should be a div with data-cy experiments-table, which will contain table inside it + cy.get('[data-cy=experiments-table]').should('exist'); + + // Wait for the table to load with data + cy.get('[data-cy=experiments-table] table tbody tr').should( + 'have.length.at.least', + 1 + ); + + // Inside the table, there should be a column with title "Instrument" + cy.get('[data-cy=experiments-table] table thead th') + .contains('Instrument') + .should('exist'); + + // This column should only contains with value Instrument 1. Make sure you check only against that column and not others + // Make sure the table is fully loaded by checking that data exists + cy.get('[data-cy=experiments-table] table tbody tr td').should('exist'); + + // Get the header row and find the Instrument column index + cy.get('[data-cy=experiments-table] table thead tr th').then( + ($headers) => { + const headers = Array.from($headers).map((header) => + Cypress.$(header).text().trim() + ); + const instrumentColumnIndex = headers.findIndex( + (header) => header === 'Instrument' + ); + + cy.log(`Found headers: ${JSON.stringify(headers)}`); + cy.log(`Instrument column index: ${instrumentColumnIndex}`); + + // Verify we found the column + expect(instrumentColumnIndex).to.be.greaterThan(-1); + + // Now check all rows in the table body for this specific column + // Use a more robust approach that re-queries the DOM each time + cy.get('[data-cy=experiments-table] table tbody tr').then(($rows) => { + const rowCount = $rows.length; + for (let i = 0; i < rowCount; i++) { + cy.get('[data-cy=experiments-table] table tbody tr') + .eq(i) + .find('td') + .eq(instrumentColumnIndex) + .should('contain.text', 'Instrument 1'); + } + }); + } + ); + }); + }); +}); From 5044b1eeaee0b69dcc7be2d3678d9f5ba9247ef1 Mon Sep 17 00:00:00 2001 From: Shivam K Date: Mon, 15 Jun 2026 16:56:49 +0200 Subject: [PATCH 3/9] chore(types): add type for permissible columns names to sort Experiments --- .../src/datasources/ExperimentDataSource.ts | 3 ++- .../mockups/ExperimentDataSource.ts | 3 ++- .../postgres/ExperimentDataSource.ts | 20 +++++++++++++------ apps/backend/src/queries/ExperimentQueries.ts | 7 +++++-- .../src/resolvers/queries/ExperimentsQuery.ts | 8 +++++++- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/datasources/ExperimentDataSource.ts b/apps/backend/src/datasources/ExperimentDataSource.ts index 2467334165..b4ca99c909 100644 --- a/apps/backend/src/datasources/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/ExperimentDataSource.ts @@ -11,6 +11,7 @@ import { ExperimentSafetyEventsRecord } from './postgres/records'; import { ExperimentsFilter, UserExperimentsFilter, + ExperimentTableSortField, } from '../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../utils/pagination'; @@ -87,7 +88,7 @@ export interface ExperimentDataSource { filter?: ExperimentsFilter, first?: number, offset?: number, - sortField?: string, + sortField?: ExperimentTableSortField, sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ diff --git a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts index 090ddd971e..2863163a56 100644 --- a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts @@ -12,6 +12,7 @@ import { SubmitExperimentSafetyArgs } from '../../resolvers/mutations/SubmitExpe import { UserExperimentsFilter, ExperimentsFilter, + ExperimentTableSortField, } from '../../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../../utils/pagination'; import { ExperimentDataSource } from '../ExperimentDataSource'; @@ -121,7 +122,7 @@ export class ExperimentDataSourceMock implements ExperimentDataSource { filter?: ExperimentsFilter, first?: number, offset?: number, - sortField?: string, + sortField?: ExperimentTableSortField, sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; experiments: Experiment[] }> { diff --git a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts index 58d42ff0ae..3d3e7a9357 100644 --- a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts @@ -13,6 +13,7 @@ import { Rejection } from '../../models/Rejection'; import { SubmitExperimentSafetyArgs } from '../../resolvers/mutations/SubmitExperimentSafetyMutation'; import { ExperimentsFilter, + ExperimentTableSortField, UserExperimentsFilter, } from '../../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../../utils/pagination'; @@ -83,10 +84,17 @@ function generateExperimentId( return `${proposalNumber}-${sequence ?? 0}`; } +type SortableExperimentTableColumnName = + | 'experiment_id' + | 'proposal_id' + | 'starts_at' + | 'ends_at'; + // Which fields we are allowed to sort on -const fieldMap: { [key: string]: string } = { - // These fields are in ExperimentsTable.tsx (frontend), - // refer Column[] and field +const fieldMap: Record< + ExperimentTableSortField, + SortableExperimentTableColumnName +> = { experimentId: 'experiment_id', 'proposal.proposalId': 'proposal_id', startsAt: 'starts_at', @@ -579,7 +587,7 @@ export default class PostgresExperimentDataSource filter?: ExperimentsFilter, first?: number, offset?: number, - sortField?: string, + sortField?: ExperimentTableSortField, sortDirection?: PaginationSortDirection, searchText?: string ): Promise<{ totalCount: number; experiments: Experiment[] }> { @@ -683,8 +691,8 @@ export default class PostgresExperimentDataSource if (!fieldMap.hasOwnProperty(sortField)) { throw new GraphQLError(`Bad sort field given: ${sortField}`); } - sortField = fieldMap[sortField]; - query.orderBy(sortField, sortDirection); + const databaseSortField = fieldMap[sortField]; + query.orderBy(databaseSortField, sortDirection); } if (first) { diff --git a/apps/backend/src/queries/ExperimentQueries.ts b/apps/backend/src/queries/ExperimentQueries.ts index 1953451bbf..a3657c8812 100644 --- a/apps/backend/src/queries/ExperimentQueries.ts +++ b/apps/backend/src/queries/ExperimentQueries.ts @@ -7,7 +7,10 @@ import { Authorized } from '../decorators'; import { Roles } from '../models/Role'; import { UserWithRole } from '../models/User'; import { ExperimentSampleArgs } from '../resolvers/queries/ExperimentSampleQuery'; -import { ExperimentsFilter } from '../resolvers/queries/ExperimentsQuery'; +import { + ExperimentsFilter, + ExperimentTableSortField, +} from '../resolvers/queries/ExperimentsQuery'; import { Experiment } from '../resolvers/types/Experiment'; import { ExperimentHasSample } from '../resolvers/types/ExperimentHasSample'; import { ExperimentSafety } from '../resolvers/types/ExperimentSafety'; @@ -97,7 +100,7 @@ export default class ExperimentQueries { filter: ExperimentsFilter = {}, first?: number, offset?: number, - sortField?: string, + sortField?: ExperimentTableSortField, sortDirection?: PaginationSortDirection, searchText?: string ) { diff --git a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts index 71b41e673d..83edcc9d79 100644 --- a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts +++ b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts @@ -24,6 +24,12 @@ class TimeSpan { to?: Date; } +export type ExperimentTableSortField = + | 'experimentId' + | 'proposal.proposalId' + | 'startsAt' + | 'endsAt'; + @InputType() export class ExperimentsFilter { @Field({ nullable: true }) @@ -60,7 +66,7 @@ export class ExperimentsArgs { public offset?: number; @Field({ nullable: true }) - public sortField?: string; + public sortField?: ExperimentTableSortField; @Field(() => PaginationSortDirection, { nullable: true }) public sortDirection?: PaginationSortDirection; From 5e909fbbaf60790c99718ab4fc970cbeb6fb7039 Mon Sep 17 00:00:00 2001 From: Shivam K Date: Mon, 15 Jun 2026 17:02:05 +0200 Subject: [PATCH 4/9] chore: add e2e for instrument sc and exp safety reviewer --- apps/e2e/cypress/e2e/experiments.cy.ts | 66 +++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/experiments.cy.ts b/apps/e2e/cypress/e2e/experiments.cy.ts index 0dcd21296a..a881fcae4f 100644 --- a/apps/e2e/cypress/e2e/experiments.cy.ts +++ b/apps/e2e/cypress/e2e/experiments.cy.ts @@ -84,7 +84,7 @@ context('Experiments tests', () => { cy.contains('1-4 of 4'); }); - it('Columns in Experiments Table are sortable', () => { + it('Columns in Experiments Table are sortable - Officer', () => { cy.login('officer'); cy.visit('/'); @@ -116,6 +116,70 @@ context('Experiments tests', () => { cy.get('span[aria-sort="descending"]').click(); }); + it('Columns in Experiments Table are sortable - Instrument Scientist', () => { + cy.login(instrumentScientist1); + cy.visit('/'); + + cy.contains('Experiments').click(); + cy.finishedLoading(); + + cy.get( + 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + }); + + it('Columns in Experiments Table are sortable - Experiment Safety Reviwer', () => { + cy.login('experimentSafetyReviewer1'); + cy.visit('/'); + + cy.contains('Experiments').click(); + cy.finishedLoading(); + + cy.get( + 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + + cy.get( + 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' + ).click(); + cy.get('span[aria-sort="ascending"]').click(); + cy.get('span[aria-sort="descending"]').click(); + }); + it('Can view visits', () => { cy.login('officer'); cy.visit('/'); From 9dece971c97cc3b721f400075fd175245d744046 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Tue, 16 Jun 2026 15:26:44 +0200 Subject: [PATCH 5/9] fix: add sort by enum to GQL endpoint --- .../postgres/ExperimentDataSource.ts | 15 +++++++-------- .../src/resolvers/queries/ExperimentsQuery.ts | 19 +++++++++++++------ .../experiment/ExperimentsTable.tsx | 19 +++++++++++++++++-- .../experiment/getAllExperiments.graphql | 2 +- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts index 3d3e7a9357..976925aa7c 100644 --- a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts @@ -90,18 +90,17 @@ type SortableExperimentTableColumnName = | 'starts_at' | 'ends_at'; -// Which fields we are allowed to sort on +// Maps a sortable column from the API to its database column. +// Instrument and experiment safety status are intentionally omitted +// (not sortable at the DB layer; create a view later if needed). const fieldMap: Record< ExperimentTableSortField, SortableExperimentTableColumnName > = { - experimentId: 'experiment_id', - 'proposal.proposalId': 'proposal_id', - startsAt: 'starts_at', - endsAt: 'ends_at', - // We don't sort on the fields below, create a view later if sorting needed - // 'instrument.name': 'instrument_id', - // 'experimentSafety.status.name': '', + [ExperimentTableSortField.experimentId]: 'experiment_id', + [ExperimentTableSortField.proposalId]: 'proposal_id', + [ExperimentTableSortField.startsAt]: 'starts_at', + [ExperimentTableSortField.endsAt]: 'ends_at', }; @injectable() diff --git a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts index 83edcc9d79..d8b65deb6b 100644 --- a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts +++ b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts @@ -8,6 +8,7 @@ import { Int, InputType, ObjectType, + registerEnumType, } from 'type-graphql'; import { ResolverContext } from '../../context'; @@ -24,11 +25,17 @@ class TimeSpan { to?: Date; } -export type ExperimentTableSortField = - | 'experimentId' - | 'proposal.proposalId' - | 'startsAt' - | 'endsAt'; +export enum ExperimentTableSortField { + experimentId = 'experimentId', + proposalId = 'proposalId', + startsAt = 'startsAt', + endsAt = 'endsAt', +} + +registerEnumType(ExperimentTableSortField, { + name: 'ExperimentTableSortField', + description: 'Experiment table columns that support sorting', +}); @InputType() export class ExperimentsFilter { @@ -65,7 +72,7 @@ export class ExperimentsArgs { @Field(() => Int, { nullable: true }) public offset?: number; - @Field({ nullable: true }) + @Field(() => ExperimentTableSortField, { nullable: true }) public sortField?: ExperimentTableSortField; @Field(() => PaginationSortDirection, { nullable: true }) diff --git a/apps/frontend/src/components/experiment/ExperimentsTable.tsx b/apps/frontend/src/components/experiment/ExperimentsTable.tsx index cbd89b4891..c1acb643f8 100644 --- a/apps/frontend/src/components/experiment/ExperimentsTable.tsx +++ b/apps/frontend/src/components/experiment/ExperimentsTable.tsx @@ -8,7 +8,12 @@ import { IconButton, Tooltip, Typography } from '@mui/material'; import React, { useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Experiment, PaginationSortDirection, SettingsId } from 'generated/sdk'; +import { + Experiment, + ExperimentTableSortField, + PaginationSortDirection, + SettingsId, +} from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; import { setSortDirectionOnSortField } from 'utils/helperFunctions'; import useDataApiWithFeedback from 'utils/useDataApiWithFeedback'; @@ -24,6 +29,14 @@ type ExperimentsTableProps = { setExperimentsFilter?: (filter: ExperimentsFilter) => void; }; +// Maps a material-table column `field` to the GraphQL sort enum. +const COLUMN_FIELD_TO_SORT_FIELD: Record = { + experimentId: ExperimentTableSortField.EXPERIMENTID, + 'proposal.proposalId': ExperimentTableSortField.PROPOSALID, + startsAt: ExperimentTableSortField.STARTSAT, + endsAt: ExperimentTableSortField.ENDSAT, +}; + const RowActionButtons = (rowData: Experiment) => { const [, setSearchParams] = useSearchParams(); @@ -111,7 +124,9 @@ export default function ExperimentsTable({ ...(experimentStartDate ? { experimentStartDate } : {}), ...(experimentEndDate ? { experimentEndDate } : {}), }, - sortField: orderBy?.orderByField, + sortField: orderBy + ? COLUMN_FIELD_TO_SORT_FIELD[orderBy.orderByField] + : undefined, sortDirection: orderBy?.orderDirection == PaginationSortDirection.ASC ? PaginationSortDirection.ASC diff --git a/apps/frontend/src/graphql/experiment/getAllExperiments.graphql b/apps/frontend/src/graphql/experiment/getAllExperiments.graphql index 8b0a14c634..9d7f06d044 100644 --- a/apps/frontend/src/graphql/experiment/getAllExperiments.graphql +++ b/apps/frontend/src/graphql/experiment/getAllExperiments.graphql @@ -2,7 +2,7 @@ query getExperiments( $filter: ExperimentsFilter $first: Int $offset: Int - $sortField: String + $sortField: ExperimentTableSortField $sortDirection: PaginationSortDirection $searchText: String ) { From b94d09af6c485ce10916e8a6d4fd17a0c0f518ca Mon Sep 17 00:00:00 2001 From: Shivam K Date: Wed, 17 Jun 2026 16:40:51 +0200 Subject: [PATCH 6/9] fix(typo): Reviewer Spelling Fixed --- apps/e2e/cypress/e2e/experiments.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/experiments.cy.ts b/apps/e2e/cypress/e2e/experiments.cy.ts index a881fcae4f..25c65b77dd 100644 --- a/apps/e2e/cypress/e2e/experiments.cy.ts +++ b/apps/e2e/cypress/e2e/experiments.cy.ts @@ -148,7 +148,7 @@ context('Experiments tests', () => { cy.get('span[aria-sort="descending"]').click(); }); - it('Columns in Experiments Table are sortable - Experiment Safety Reviwer', () => { + it('Columns in Experiments Table are sortable - Experiment Safety Reviewer', () => { cy.login('experimentSafetyReviewer1'); cy.visit('/'); From ea47443089f981408d6021f44e8022ce3c45dd74 Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:04:04 +0200 Subject: [PATCH 7/9] fix: add review suggestions --- .../src/datasources/ExperimentDataSource.ts | 2 +- .../mockups/ExperimentDataSource.ts | 2 +- .../postgres/ExperimentDataSource.ts | 2 +- apps/backend/src/models/Experiment.ts | 9 ++ apps/backend/src/queries/ExperimentQueries.ts | 6 +- .../src/resolvers/queries/ExperimentsQuery.ts | 18 +--- apps/backend/src/resolvers/registerEnums.ts | 5 + apps/e2e/cypress/e2e/experiments.cy.ts | 101 ++++++------------ 8 files changed, 54 insertions(+), 91 deletions(-) diff --git a/apps/backend/src/datasources/ExperimentDataSource.ts b/apps/backend/src/datasources/ExperimentDataSource.ts index b4ca99c909..c025e6a3d3 100644 --- a/apps/backend/src/datasources/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/ExperimentDataSource.ts @@ -2,6 +2,7 @@ import { Experiment, ExperimentHasSample, ExperimentSafety, + ExperimentTableSortField, InstrumentScientistDecisionEnum, ExperimentSafetyReviewerDecisionEnum, } from '../models/Experiment'; @@ -11,7 +12,6 @@ import { ExperimentSafetyEventsRecord } from './postgres/records'; import { ExperimentsFilter, UserExperimentsFilter, - ExperimentTableSortField, } from '../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../utils/pagination'; diff --git a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts index 2863163a56..1c46ef9de4 100644 --- a/apps/backend/src/datasources/mockups/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/mockups/ExperimentDataSource.ts @@ -3,6 +3,7 @@ import { ExperimentSafety, ExperimentHasSample, ExperimentStatus, + ExperimentTableSortField, InstrumentScientistDecisionEnum, ExperimentSafetyReviewerDecisionEnum, } from '../../models/Experiment'; @@ -12,7 +13,6 @@ import { SubmitExperimentSafetyArgs } from '../../resolvers/mutations/SubmitExpe import { UserExperimentsFilter, ExperimentsFilter, - ExperimentTableSortField, } from '../../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../../utils/pagination'; import { ExperimentDataSource } from '../ExperimentDataSource'; diff --git a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts index 9655e42377..a8767b79d0 100644 --- a/apps/backend/src/datasources/postgres/ExperimentDataSource.ts +++ b/apps/backend/src/datasources/postgres/ExperimentDataSource.ts @@ -7,13 +7,13 @@ import { ExperimentHasSample, ExperimentSafety, ExperimentSafetyReviewerDecisionEnum, + ExperimentTableSortField, InstrumentScientistDecisionEnum, } from '../../models/Experiment'; import { Rejection } from '../../models/Rejection'; import { SubmitExperimentSafetyArgs } from '../../resolvers/mutations/SubmitExperimentSafetyMutation'; import { ExperimentsFilter, - ExperimentTableSortField, UserExperimentsFilter, } from '../../resolvers/queries/ExperimentsQuery'; import { PaginationSortDirection } from '../../utils/pagination'; diff --git a/apps/backend/src/models/Experiment.ts b/apps/backend/src/models/Experiment.ts index c5a9fef515..74af03df60 100644 --- a/apps/backend/src/models/Experiment.ts +++ b/apps/backend/src/models/Experiment.ts @@ -4,6 +4,15 @@ export enum ExperimentStatus { COMPLETED = 'COMPLETED', } +// Experiment table columns that support sorting. Lives in the model so it can +// be reused across queries/datasources without importing from the resolver layer. +export enum ExperimentTableSortField { + experimentId = 'experimentId', + proposalId = 'proposalId', + startsAt = 'startsAt', + endsAt = 'endsAt', +} + export class Experiment { constructor( public experimentPk: number, diff --git a/apps/backend/src/queries/ExperimentQueries.ts b/apps/backend/src/queries/ExperimentQueries.ts index a3657c8812..f822080d2f 100644 --- a/apps/backend/src/queries/ExperimentQueries.ts +++ b/apps/backend/src/queries/ExperimentQueries.ts @@ -4,13 +4,11 @@ import { UserAuthorization } from '../auth/UserAuthorization'; import { Tokens } from '../config/Tokens'; import { ExperimentDataSource } from '../datasources/ExperimentDataSource'; import { Authorized } from '../decorators'; +import { ExperimentTableSortField } from '../models/Experiment'; import { Roles } from '../models/Role'; import { UserWithRole } from '../models/User'; import { ExperimentSampleArgs } from '../resolvers/queries/ExperimentSampleQuery'; -import { - ExperimentsFilter, - ExperimentTableSortField, -} from '../resolvers/queries/ExperimentsQuery'; +import { ExperimentsFilter } from '../resolvers/queries/ExperimentsQuery'; import { Experiment } from '../resolvers/types/Experiment'; import { ExperimentHasSample } from '../resolvers/types/ExperimentHasSample'; import { ExperimentSafety } from '../resolvers/types/ExperimentSafety'; diff --git a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts index d8b65deb6b..657efdf514 100644 --- a/apps/backend/src/resolvers/queries/ExperimentsQuery.ts +++ b/apps/backend/src/resolvers/queries/ExperimentsQuery.ts @@ -8,11 +8,13 @@ import { Int, InputType, ObjectType, - registerEnumType, } from 'type-graphql'; import { ResolverContext } from '../../context'; -import { ExperimentStatus } from '../../models/Experiment'; +import { + ExperimentStatus, + ExperimentTableSortField, +} from '../../models/Experiment'; import { PaginationSortDirection } from '../../utils/pagination'; import { Experiment } from '../types/Experiment'; @@ -25,18 +27,6 @@ class TimeSpan { to?: Date; } -export enum ExperimentTableSortField { - experimentId = 'experimentId', - proposalId = 'proposalId', - startsAt = 'startsAt', - endsAt = 'endsAt', -} - -registerEnumType(ExperimentTableSortField, { - name: 'ExperimentTableSortField', - description: 'Experiment table columns that support sorting', -}); - @InputType() export class ExperimentsFilter { @Field({ nullable: true }) diff --git a/apps/backend/src/resolvers/registerEnums.ts b/apps/backend/src/resolvers/registerEnums.ts index da52202294..0d5e076b97 100644 --- a/apps/backend/src/resolvers/registerEnums.ts +++ b/apps/backend/src/resolvers/registerEnums.ts @@ -9,6 +9,7 @@ import { import { ExperimentSafetyReviewerDecisionEnum, ExperimentStatus, + ExperimentTableSortField, InstrumentScientistDecisionEnum, } from '../models/Experiment'; import { FapReviewVisibility } from '../models/Fap'; @@ -70,6 +71,10 @@ export const registerEnums = () => { registerEnumType(ExperimentStatus, { name: 'ExperimentStatus', }); + registerEnumType(ExperimentTableSortField, { + name: 'ExperimentTableSortField', + description: 'Experiment table columns that support sorting', + }); registerEnumType(FeedbackStatus, { name: 'FeedbackStatus', }); diff --git a/apps/e2e/cypress/e2e/experiments.cy.ts b/apps/e2e/cypress/e2e/experiments.cy.ts index 25c65b77dd..0e0a9ca083 100644 --- a/apps/e2e/cypress/e2e/experiments.cy.ts +++ b/apps/e2e/cypress/e2e/experiments.cy.ts @@ -6,6 +6,33 @@ import { import featureFlags from '../support/featureFlags'; import initialDBData from '../support/initialDBData'; +// Clicks through each sortable column of the Experiments table and asserts that +// the sort state lands in the URL. Those `sortField`/`sortDirection` params are +// what get forwarded as the getExperiments query variables, so this catches a +// regression in the column -> sort-field wiring, not just a header that stops +// toggling visually. +const assertExperimentsTableColumnsAreSortable = () => { + const sortableColumns = [ + { draggableId: '1', sortField: 'experimentId' }, + { draggableId: '2', sortField: 'proposal.proposalId' }, + { draggableId: '3', sortField: 'startsAt' }, + { draggableId: '4', sortField: 'endsAt' }, + ]; + + sortableColumns.forEach(({ draggableId, sortField }) => { + cy.get( + `div[data-rfd-draggable-id="${draggableId}"] [data-testid="mtableheader-sortlabel"]` + ).click(); + cy.url().should('include', `sortField=${sortField}`); + cy.url().should('include', 'sortDirection=asc'); + + cy.get('span[aria-sort="ascending"]').click(); + cy.url().should('include', 'sortDirection=desc'); + + cy.get('span[aria-sort="descending"]').click(); + }); +}; + context('Experiments tests', () => { const instrumentScientist1 = initialDBData.users.instrumentScientist1; @@ -91,29 +118,7 @@ context('Experiments tests', () => { cy.get('[data-cy=officer-menu-items]').contains('Experiments').click(); cy.finishedLoading(); - cy.get( - 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); + assertExperimentsTableColumnsAreSortable(); }); it('Columns in Experiments Table are sortable - Instrument Scientist', () => { @@ -123,29 +128,7 @@ context('Experiments tests', () => { cy.contains('Experiments').click(); cy.finishedLoading(); - cy.get( - 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); + assertExperimentsTableColumnsAreSortable(); }); it('Columns in Experiments Table are sortable - Experiment Safety Reviewer', () => { @@ -155,29 +138,7 @@ context('Experiments tests', () => { cy.contains('Experiments').click(); cy.finishedLoading(); - cy.get( - 'div[data-rfd-draggable-id="1"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="2"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="3"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); - - cy.get( - 'div[data-rfd-draggable-id="4"] [data-testid="mtableheader-sortlabel"]' - ).click(); - cy.get('span[aria-sort="ascending"]').click(); - cy.get('span[aria-sort="descending"]').click(); + assertExperimentsTableColumnsAreSortable(); }); it('Can view visits', () => { @@ -213,7 +174,7 @@ context('Experiments tests', () => { tableValue = [...tableValue, $el.text().toString()]; }) .then(() => { - // Explanation: The table has 7 columns. We will sort each column in ascending and descending order and check if the table is sorted correctly. + // Explanation: The table has 6 columns. We will sort each column in ascending and descending order and check if the table is sorted correctly. // tableColumns: Array of objects. Each object contains the title of the column and the data of the column in original, ascending and descending order. const tableColumns = [ { From 6c9feddb845c483ef8a2626fda55ea00e257fc41 Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:22:18 +0200 Subject: [PATCH 8/9] chore: remove debug port from package.json --- apps/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index a28da1a9c7..8f131bfc3f 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -19,7 +19,7 @@ "build:validation": "npm --prefix ../../validation ci && npm --prefix ../../validation run build", "build": "npm run build:validation && npm run generate:uows && rm -rf ./build && tsc", "postinstall": "npm run build:validation", - "dev": "docker compose up -d --remove-orphans && ts-node-dev --inspect=9229 -r tsconfig-paths/register --respawn -T ./index.ts", + "dev": "docker compose up -d --remove-orphans && ts-node-dev -r tsconfig-paths/register --respawn -T ./index.ts", "start:e2e": "docker compose -f docker-compose.e2e.yml up -d --remove-orphans && wait-on --verbose --interval 5000 --delay 15000 --timeout 300000 tcp:5432 && node ./build/index.js", "start:e2e:stfc": "docker compose -f docker-compose-stfc.e2e.yml up -d --remove-orphans && wait-on --verbose --interval 5000 --delay 15000 --timeout 300000 tcp:5432 && node ./build/index.js", "dev:docker": "npm install && ts-node-dev --inspect=0.0.0.0:9229 --respawn -T ./index.ts", From f146692677203801ba2b77e8eba8d084e54eba9d Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:32:36 +0200 Subject: [PATCH 9/9] chore: remove .zed --- .zed/debug.json | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .zed/debug.json diff --git a/.zed/debug.json b/.zed/debug.json deleted file mode 100644 index bcfa80b72b..0000000000 --- a/.zed/debug.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "label": "Launch Debug React in Chrome", - "adapter": "JavaScript", - "type": "chrome", - "request": "launch", - "console": "integratedTerminal", - "url": "http://localhost:3000", - "webRoot": "$ZED_WORKTREE_ROOT/apps/frontend", - "skipFiles": ["/**"], - }, - { - "label": "Debug Local Backend {attach} (npm run dev)", - "adapter": "JavaScript", - "type": "node", - "request": "attach", - "port": 9229, - "address": "localhost", - "restart": true, - "localRoot": "$ZED_WORKTREE_ROOT/apps/backend", - "skipFiles": ["/**"], - }, -]