Skip to content

feat: allow inviting Data Access Users to Proposal with email - #1615

Open
shivoomiess wants to merge 51 commits into
developfrom
SWAP-5408-invite-DAU
Open

feat: allow inviting Data Access Users to Proposal with email#1615
shivoomiess wants to merge 51 commits into
developfrom
SWAP-5408-invite-DAU

Conversation

@shivoomiess

@shivoomiess shivoomiess commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces the capability to invite data access users to a proposal by creating a new data access claim.

Motivation and Context

This change is required to allow data access users to be linked directly to proposals, enabling them to view and manipulate data related to the proposal they are invited to. This enhances collaboration and data management in the project.

Changes

  1. A new table 'data_access_claims' is created in the database to store the data access user claims.
  2. The 'DataAccessClaimDataSource' token is added to various configuration files to enable the data access claim functionality across different environments.
  3. The 'DataAccessClaimDataSource' is mapped to 'PostgresDataAccessClaimDataSource' in the dependency configuration files to tie the functionality with the PostgreSQL database.
  4. GraphQL queries and mutations have been added on the frontend and backend for getting / setting DAU email invites.
  5. The read/write path for creating invites has been mirrored from the existing code for Co Proposer Invites and corresponding mutations have been added.
  6. On the frontend Proposal Summary page, we now see invited emails inline for both Co-Proposers and Data Access Users instead of a separate row for only co-proposers.

Visual Changes

The invite pop-ups have been updated to look clean and show the type of proposal invite (Co-Proposer / Data Access).

Previous

Screenshot 2026-07-06 at 15 48 27

Current

Screenshot 2026-07-06 at 16 02 22

How Has This Been Tested?

Adding and Deleting Invited Users

  1. Changes are saved to the DB
Screen.Recording.2026-07-02.at.19.51.10.mov
Screen.Recording.2026-07-02.at.19.51.42.mov
  1. Emails are sent correctly
Screen.Recording.2026-07-02.at.19.53.53.mov

Invite Popups for New Sign-Ups

Screen.Recording.2026-07-06.at.16.15.20.mov

Fixes Jira Issue

https://jira.ess.eu//browse/SWAP-5408

Depends On

Tests included/Docs Updated?

  • I have added tests to cover my changes.
  • All relevant doc has been updated

@shivoomiess
shivoomiess requested a review from a team as a code owner June 30, 2026 14:37
@shivoomiess
shivoomiess requested review from simonfernandes and removed request for a team June 30, 2026 14:37
@shivoomiess shivoomiess changed the title feat: Invite Data Access Users to Proposal feat: allow inviting Data Access Users to Proposal with email Jun 30, 2026

@yoganandaness yoganandaness left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shivoomiess Kindly attach relevant video or screenshot

Comment thread apps/backend/src/datasources/postgres/records.ts Outdated
Comment thread apps/backend/src/datasources/postgres/DataAccessUsersDataSource.ts Outdated
Comment thread apps/backend/src/datasources/postgres/DataAccessUsersDataSource.ts Outdated
Comment thread apps/backend/src/datasources/DataAccessUsersDataSource.ts

@yoganandaness yoganandaness left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. kindly test all pieces of change
  2. Attach video or pic if possible
  3. Pls have clean diff

Comment thread apps/backend/src/eventHandlers/logging.ts
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts
Comment thread apps/backend/src/mutations/InviteMutations.ts

@jekabs-karklins jekabs-karklins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over all looks very good :raisedhands:
As this is a bigger PR I think you will get a bit more comments :).

I have checked the PR and left some comments, and Happy to chat if any help or clarification needed.

Comment thread apps/backend/src/queries/InviteQueries.ts Outdated
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts Outdated
Comment thread apps/backend/src/mutations/InviteMutations.ts Outdated

@shivoomiess shivoomiess left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incorporated feedback

The invites.cy.ts test "Should be able to view and accept the outstanding
invites with the proposal information" had failed on every CI run of this
branch since 30 June. After clicking accept, the proposal table still read
"No records to display" where the test expected the accepted proposal, so
it only ever failed after a 30s retry, which made it look like timing
noise rather than a defect.

The accept helpers fired the mutation and returned immediately, so the
caller ran onAccept() while the request was still in flight. The proposal
table refetched before the invite had been accepted and came back empty,
which is what the e2e test saw.

The success snackbar had the same problem, and the .catch() that rethrew
inside the promise chain surfaced as an unhandled rejection rather than
reaching the component, so a failed acceptance still reported success.
That is why nothing in the UI hinted at the real cause.

Return the promises and await them at the call sites.
Comment thread apps/backend/db_patches/0213_AddDataAccessClaims.sql Outdated
Comment thread apps/frontend/src/components/proposal/DataAccessUsersModal.tsx Outdated
Comment thread apps/backend/src/mutations/InviteMutations.ts Outdated
- Renumber the data access claims patch to 0214, as 0213 is taken
- Block the data access users modal until the proposal has loaded, and
  reset the loading flag when the proposal request fails so the modal
  cannot get stuck on "Loading..."
setDataAccessInvites rebuilds the invite list by diffing the emails the
frontend submits against the invites already attached to the proposal.
That set included claimed and expired invites, while the read path
(Proposal.dataAccessInvites) only returns pending ones, so an accepted
invite was never in the submitted list and got deleted on the next
update.

Take the existing set from the invite data source filtered on
isClaimed: false, which is the same set the read path returns. Both
halves of the diff now operate on the same invites, so claimed invites
are left alone and pending ones stay deletable.
setCoProposerInvites has the same defect as setDataAccessInvites did: it
diffed the submitted emails against every invite claimed for the
proposal, claimed ones included, while Proposal.coProposerInvites only
returns pending invites. An accepted co-proposer invite was therefore
absent from the submitted list and deleted on the next update.

Take the existing set from the invite data source filtered on
isClaimed: false, matching the read path, so both halves of the diff see
the same invites.
…n B)

The proposal-scoped lookups moved to InviteDataSource.getXInvites, so
findByProposalPk has no production caller left. Remove it from both
claim data source interfaces and their postgres implementations, and
keep it on the mocks only.

InviteDataSourceMock held its own copy of the claim rows for the
getXInvites proposalPk filter while findXInvites and the mutations wrote
through the claim data sources, so the two stores could disagree. Drop
the internal copies and resolve the proposal scope through the injected
claim mocks, which makes claims created during a test visible to every
read path. Reconciling the seeds means CoProposerClaimDataSourceMock
also has to claim invite 2 on proposal 1, DataAccessClaimDataSourceMock
has to point at invite 4 rather than the non-existent invite 7, and
InviteMutations.spec has to init the data access claim mock it never
initialised before.
create() derived the new id from the array length, so once a mutation
had deleted an invite the next create handed out an id that another
invite still held. findById then resolved the older invite, which only
became visible now that claims created during a test are read back
through the claim mocks.
The read path and the setXInvites mutations each decided for themselves
what a pending invite on a proposal is: InviteQueries called
findXInvites(proposalPk, false) while the mutations called
getXInvites({ proposalPk, isClaimed: false }). Two spellings of the same
rule against two different data source methods is what let them drift
apart in the first place, which is the bug this branch started from.

Replace both with findPendingCoProposerInvites / findPendingDataAccessInvites
on InviteDataSource. Neither caller states the filter now, so neither can
disagree about it. This matches how the visit path already works, where
the query and the mutation share findVisitRegistrationInvites.

Dropping the isClaimed flag also fixes the mock, which took the argument
and ignored it, so mocked reads returned claimed invites the postgres
implementation would have filtered out.
Comment thread apps/backend/src/mutations/InviteMutations.ts
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts
Comment thread apps/backend/src/mutations/InviteMutations.ts
Comment thread apps/backend/src/eventHandlers/email/essEmailHandler.ts Outdated
@jekabs-karklins

Copy link
Copy Markdown
Contributor

I thiks otherwise this looks very good, just let me know your thoughts on the comments I mentioned

@shivoomiess

Copy link
Copy Markdown
Collaborator Author

@zacharyjhankin @simonfernandes we need one more review before we merge this, could you please take a look? my summer engagement at ESS is concluding next week

@yoganandaness
yoganandaness requested review from TCMeldrum and jekabs-karklins and removed request for simonfernandes August 25, 2026 12:38

@jekabs-karklins jekabs-karklins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now, looking forward to have this feature on production 👍🏻

@mutambaraf
mutambaraf self-requested a review August 27, 2026 13:08

@mutambaraf mutambaraf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and left few comments


export const dummyCountry = new Country(1, 'Denmark');
export const dummyCountry2 = new Country(2, 'United Kingdom');
export const dummyCountry3 = new Country(3, 'Belarus');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove dummyCountry3 because it is not being used.

constructor(
@inject(Tokens.CoProposerClaimDataSource)
private coProposerDataSource: CoProposerClaimDataSource
private coProposerDataSource: CoProposerClaimDataSourceMock,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can improve naming to coProposerClaimDataSource

coProposerClaims.map((claim) => this.findById(claim.inviteId))
);
async findPendingCoProposerInvites(proposalPk: number): Promise<Invite[]> {
return this.getCoProposerInvites({ proposalPk, isClaimed: false });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe lets avoid hard coding this variable by actually passing in through findPendingCoProposerInvites

}
})
findPendingCoProposerInvites(proposalPk: number): Promise<Invite[]> {
return this.getCoProposerInvites({ proposalPk, isClaimed: false });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also avoid hard coding isClaimed: false

Comment on lines +1467 to +1471
export interface DataAccessClaimRecord {
readonly invite_id: number;
readonly proposal_pk: number;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this if we are not using it.

.insert({ invite_id: inviteId, proposal_pk: proposalPk })
.returning('*')
.then((rows) => {
const row = rows[0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to check if rows is an array and I think this is where we where supposed to use DataAccessClaimRecord

): Promise<Invite[]> {
const inviteIdsOnProposal = filter.proposalPk
? (
await this.coProposerDataSource.findByProposalPk(filter.proposalPk)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be more clear if we assign the value of await this.coProposerDataSource.findByProposalPk(filter.proposalPk) to a variable an check if it is returned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants