diff --git a/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js b/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js index ea3b6af4a4..1c5e235cec 100644 --- a/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js @@ -56,44 +56,34 @@ describe('Submitted reports', () => { const submissions = submittedReports.data.submissions; - cy.get('[data-cy="submissions"] > div').should('have.length', submissions.length); + cy.get('[data-cy="submissions"] [data-cy="row"]').should('have.length', submissions.length); submissions.forEach((report, index) => { - cy.get('[data-cy="submissions"]') - .children(`:nth-child(${index + 1})`) - .within(() => { - cy.get('[data-cy="review-button"]').click(); + cy.get('[data-cy="submissions"] [data-cy="row"]') + .eq(index) + .then((element) => { + cy.wrap(element) + .find('[data-cy="cell"] [data-cy="review-submission"]') + .should('not.exist'); }); - cy.get('[data-cy="submissions"]') - .children(`:nth-child(${index + 1})`) - .within(() => { - const keys = [ - 'source_domain', - 'authors', - 'submitters', - 'incident_id', - 'date_published', - 'date_submitted', - 'date_downloaded', - 'date_modified', - 'url', - 'incident_ids', - ]; - - for (const key of keys) { - if (report[key]) { - let value = report[key]; - - if (isArray(value)) { - value = arrayToList(value); + cy.get('[data-cy="submissions"] [data-cy="row"]') + .eq(index) + .then((element) => { + const keys = ['title', 'submitters', 'incident_date', 'editor', 'status']; + + cy.wrap(element) + .find('[data-cy="cell"]') + .each((cell, cellIndex) => { + if (report[keys[cellIndex]]) { + let value = report[keys[cellIndex]]; + + if (isArray(value)) { + value = arrayToList(value); + } + cy.wrap(cell).should('contain', value); } - - cy.get(`[data-cy="${key}"] div:nth-child(2)`).should('contain', value); - } else { - cy.get(`[data-cy="${key}"] div:nth-child(2)`).should('not.exist'); - } - } + }); }); }); }); @@ -116,6 +106,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -131,13 +132,11 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.wait('@AllQuickAdd'); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.waitForStableDOM(); cy.conditionalIntercept( '**/graphql', @@ -183,7 +182,11 @@ describe('Submitted reports', () => { } ); - cy.get('@promoteForm').contains('button', 'Add new Incident').click(); + cy.get('select[data-cy="promote-select"]').as('dropdown'); + + cy.get('@dropdown').select('Incident'); + + cy.get('[data-cy="promote-button"]').click(); cy.wait('@promoteSubmission') .its('request.body.variables.input') @@ -241,6 +244,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -256,13 +270,11 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); + cy.visit(url + `?editSubmission=${submission._id}}`); - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.wait('@AllQuickAdd'); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.waitForStableDOM(); cy.conditionalIntercept( '**/graphql', @@ -291,7 +303,7 @@ describe('Submitted reports', () => { } ); - cy.get('@promoteForm').contains('button', 'Add to incident 10').click(); + cy.get('[data-cy="promote-to-report-button"]').click(); cy.wait('@promoteSubmission') .its('request.body.variables.input') @@ -337,6 +349,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -352,13 +375,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.visit(url + `?editSubmission=${submission._id}}`); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.wait('@AllQuickAdd'); cy.conditionalIntercept( '**/graphql', @@ -404,7 +423,7 @@ describe('Submitted reports', () => { } ); - cy.get('@promoteForm').contains('button', 'Add to incidents 52 and 53').click(); + cy.get('[data-cy="promote-to-report-button"]').click(); cy.wait('@promoteSubmission') .its('request.body.variables.input') @@ -467,6 +486,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -482,13 +512,11 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.wait('@AllQuickAdd'); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.waitForStableDOM(); cy.conditionalIntercept( '**/graphql', @@ -504,7 +532,11 @@ describe('Submitted reports', () => { } ); - cy.get('@promoteForm').contains('button', 'Add as issue').click(); + cy.get('select[data-cy="promote-select"]').as('dropdown'); + + cy.get('@dropdown').select('Issue'); + + cy.get('[data-cy="promote-button"]').click(); cy.wait('@promoteSubmission') .its('request.body.variables.input') @@ -537,6 +569,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -552,13 +595,11 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.wait('@AllQuickAdd'); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.waitForStableDOM(); cy.conditionalIntercept( '**/graphql', @@ -571,13 +612,11 @@ describe('Submitted reports', () => { } ); - cy.get('@promoteForm').contains('button', 'Reject New Report').click(); + cy.get('[data-cy="reject-button"]').click(); cy.wait('@DeleteSubmission').then((xhr) => { expect(xhr.request.body.variables._id).to.eq('6123bf345e740c1a81850e89'); }); - - cy.get('[data-cy="submissions"]').children().should('have.length', 0); }); maybeIt('Edits a submission - update just a text', () => { @@ -619,15 +658,7 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.visit(url + `?editSubmission=${submittedReports.data.submissions[0]._id}`); cy.setEditorText( '## Another one\n\n**More markdown**\n\nAnother paragraph with more text to reach the minimum character count!' @@ -676,8 +707,6 @@ describe('Submitted reports', () => { cy.clock(now); - cy.get('@modal').contains('Update').click(); - cy.wait('@UpsertGoogle').its('request.body.variables.entity.entity_id').should('eq', 'google'); cy.wait('@UpsertAdults').its('request.body.variables.entity.entity_id').should('eq', 'adults'); @@ -697,6 +726,7 @@ describe('Submitted reports', () => { description: 'By NEIL BEDI and KATHLEEN McGRORY\nTimes staff writers\nNov. 19, 2020\nThe Pasco Sheriff’s Office keeps a secret list of kids it thinks could “fall into a life of crime” based on factors like wheth', image_url: 'https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg', + incident_title: 'Submisssion 1 title', incident_date: '2015-09-01', incident_ids: [], language: 'en', @@ -721,8 +751,6 @@ describe('Submitted reports', () => { }, }); }); - - cy.get('@modal').should('not.exist'); }); maybeIt('Edits a submission - uses fetch info', () => { @@ -752,11 +780,7 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.visit(url + `?editSubmission=${submittedReports.data.submissions[0]._id}`); cy.conditionalIntercept( '**/graphql', @@ -772,10 +796,6 @@ describe('Submitted reports', () => { } ); - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); - cy.get('input[name="url"]').click(); cy.clickOutside(); @@ -818,6 +838,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -833,13 +864,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.wait('@AllQuickAdd'); cy.on('fail', (err) => { expect(err.message).to.include( @@ -854,7 +881,11 @@ describe('Submitted reports', () => { {} ); - cy.get('@promoteForm').contains('button', 'Add new Incident').click(); + cy.get('select[data-cy="promote-select"]').as('dropdown'); + + cy.get('@dropdown').select('Incident'); + + cy.get('[data-cy="promote-button"]').click(); cy.contains('[data-cy="toast"]', 'Description is required.').should('exist'); @@ -882,6 +913,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -897,13 +939,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.wait('@AllQuickAdd'); cy.on('fail', (err) => { expect(err.message).to.include( @@ -918,7 +956,11 @@ describe('Submitted reports', () => { {} ); - cy.get('@promoteForm').contains('button', 'Add as issue').click(); + cy.get('select[data-cy="promote-select"]').as('dropdown'); + + cy.get('@dropdown').select('Issue'); + + cy.get('[data-cy="promote-button"]').click(); cy.contains('[data-cy="toast"]', 'Title is required').should('exist'); @@ -946,6 +988,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'AllQuickAdd', @@ -961,13 +1014,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.wait('@AllQuickAdd'); cy.on('fail', (err) => { expect(err.message).to.include( @@ -982,7 +1031,7 @@ describe('Submitted reports', () => { {} ); - cy.get('@promoteForm').contains('button', 'Add to incident 12').click(); + cy.get('[data-cy="promote-to-report-button"]').contains('Add to incident 12').click(); cy.contains('[data-cy="toast"]', '*Date is not valid, must be `YYYY-MM-DD`').should('exist'); @@ -990,7 +1039,8 @@ describe('Submitted reports', () => { } ); - maybeIt('Should display an error message if data is missing', () => { + it.skip('Should display an error message if data is missing', () => { + // With new submission list, we allow to save changes always cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); const submission = submittedReports.data.submissions.find( @@ -1008,6 +1058,17 @@ describe('Submitted reports', () => { } ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'FindSubmission', + 'FindSubmission', + { + data: { + submission: submission, + }, + } + ); + cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName == 'FindSubmission', @@ -1023,19 +1084,13 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('@modal').contains('Please review submission. Some data is missing.').should('exist'); + cy.get('[data-cy="submission-form"]') + .contains('Please review submission. Some data is missing.') + .should('exist'); - cy.get('[data-cy="update-btn"]').should('be.disabled'); + cy.get('[data-cy="submission"]').contains('Changes not saved').should('exist'); cy.waitForStableDOM(); @@ -1053,14 +1108,14 @@ describe('Submitted reports', () => { cy.get('input[name=date_downloaded]').type('2023-01-01'); - cy.get('@modal') + cy.get('[data-cy="submission-form"]') .contains('Please review submission. Some data is missing.') .should('not.exist'); - cy.get('[data-cy="update-btn"]').should('not.be.disabled'); + cy.get('[data-cy="submission"]').contains('Changes not saved').should('not.exist'); }); - maybeIt('Should display submission image on edit modal', () => { + maybeIt('Should display submission image on edit page', () => { cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); const submission = submittedReports.data.submissions.find( @@ -1118,17 +1173,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.wait('@AllQuickAdd'); cy.waitForStableDOM(); @@ -1181,17 +1228,9 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.wait('@AllQuickAdd'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); + cy.visit(url + `?editSubmission=${submission._id}`); - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.wait('@AllQuickAdd'); cy.get('[data-cy="image-preview-figure"] canvas').should('exist'); }); @@ -1229,25 +1268,13 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.visit(url + `?editSubmission=${submission._id}`); cy.waitForStableDOM(); cy.get('input[name=date_published]').type('3000-01-01'); - cy.get('@modal').contains('*Date must be in the past').should('exist'); - - cy.get('@modal').contains('Please review submission. Some data is missing.').should('exist'); - - cy.get('[data-cy="update-btn"]').should('be.disabled'); + cy.get('[data-cy="submission-form"]').contains('*Date must be in the past').should('exist'); }); maybeIt('Should display an error message if Date Downloaded is not in the past', () => { @@ -1283,25 +1310,13 @@ describe('Submitted reports', () => { cy.wait('@FindSubmissions'); - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); + cy.visit(url + `?editSubmission=${submission._id}`); cy.waitForStableDOM(); cy.get('input[name=date_downloaded]').type('3000-01-01'); - cy.get('@modal').contains('*Date must be in the past').should('exist'); - - cy.get('@modal').contains('Please review submission. Some data is missing.').should('exist'); - - cy.get('[data-cy="update-btn"]').should('be.disabled'); + cy.get('[data-cy="submission-form"]').contains('*Date must be in the past').should('exist'); }); maybeIt( @@ -1359,32 +1374,6 @@ describe('Submitted reports', () => { } ); - cy.visit(url); - - cy.wait('@FindSubmissions'); - - cy.get('[data-cy="submission"]').first().as('promoteForm'); - - cy.get('@promoteForm').within(() => { - cy.get('[data-cy="review-button"]').click(); - }); - - cy.get('[data-cy="edit-submission"]').eq(0).click(); - - cy.get('[data-cy="submission-modal"]').as('modal').should('be.visible'); - - cy.waitForStableDOM(); - - cy.get(`input[name="incident_ids"]`).type('1'); - - cy.waitForStableDOM(); - - cy.get(`[role="option"]`).first().click(); - - cy.waitForStableDOM(); - - cy.get('[data-cy="incident-data-section"]').should('not.exist'); - cy.conditionalIntercept( '**/graphql', (req) => req.body.operationName === 'UpdateSubmission', @@ -1425,15 +1414,23 @@ describe('Submitted reports', () => { } ); - cy.get('@modal').contains('Update').click(); + cy.visit(url); - cy.wait('@UpsertGoogle') - .its('request.body.variables.entity.entity_id') - .should('eq', 'google'); + cy.wait('@FindSubmissions'); - cy.wait('@UpsertAdults') - .its('request.body.variables.entity.entity_id') - .should('eq', 'adults'); + cy.visit(url + `?editSubmission=${submittedReports.data.submissions[0]._id}`); + + cy.waitForStableDOM(); + + cy.get(`input[name="incident_ids"]`).type('1'); + + cy.waitForStableDOM(); + + cy.get(`[role="option"]`).first().click(); + + cy.waitForStableDOM(); + + cy.get('[data-cy="incident-data-section"]').should('not.exist'); cy.wait('@UpdateSubmission').then((xhr) => { expect(xhr.request.body.variables.query).to.deep.nested.include({ @@ -1445,7 +1442,13 @@ describe('Submitted reports', () => { }); }); - cy.get('@modal').should('not.exist'); + cy.wait('@UpsertGoogle') + .its('request.body.variables.entity.entity_id') + .should('eq', 'google'); + + cy.wait('@UpsertAdults') + .its('request.body.variables.entity.entity_id') + .should('eq', 'adults'); } ); }); diff --git a/site/gatsby-site/cypress/e2e/integration/pages.cy.js b/site/gatsby-site/cypress/e2e/integration/pages.cy.js index c507e007c3..ead36103eb 100644 --- a/site/gatsby-site/cypress/e2e/integration/pages.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/pages.cy.js @@ -122,6 +122,7 @@ describe('Pages', () => { if (selectorExists) { cy.get('[data-cy="cloudinary-image-wrapper"]').each(($el) => { cy.wrap($el) + .scrollIntoView() .find('[data-cy="cloudinary-image"]') .should('have.attr', 'src') .then(($src) => { diff --git a/site/gatsby-site/cypress/e2e/integration/socialShareButtons.cy.js b/site/gatsby-site/cypress/e2e/integration/socialShareButtons.cy.js index d962f5599b..2ce4d4b2d5 100644 --- a/site/gatsby-site/cypress/e2e/integration/socialShareButtons.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/socialShareButtons.cy.js @@ -51,6 +51,10 @@ describe('Social Share buttons on pages', { retries: { runMode: 4 } }, () => { cy.get('[data-cy=btn-share-twitter]').first().click(); cy.get('@popup_twitter', { timeout: 8000 }).should('be.called'); + cy.url().should( + 'contain', + `https://twitter.com/i/flow/login?redirect_after_login=%2Fintent%2Ftweet%3Ftext%3D` + ); cy.url().should('contain', `url%3D${encodeURIComponent(canonicalUrl)}`); }); diff --git a/site/gatsby-site/cypress/e2e/integration/submit.cy.js b/site/gatsby-site/cypress/e2e/integration/submit.cy.js index 482c57da7f..3d9940ef4a 100644 --- a/site/gatsby-site/cypress/e2e/integration/submit.cy.js +++ b/site/gatsby-site/cypress/e2e/integration/submit.cy.js @@ -1,5 +1,3 @@ -import { isArray } from 'lodash'; -import { arrayToList } from '../../../src/utils/typography'; import parseNews from '../../fixtures/api/parseNews.json'; import semanticallyRelated from '../../fixtures/api/semanticallyRelated.json'; import probablyRelatedIncidents from '../../fixtures/incidents/probablyRelatedIncidents.json'; @@ -401,41 +399,16 @@ describe('The Submit form', () => { cy.contains('Report successfully added to review queue').should('exist'); + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + cy.visit('/apps/submitted'); cy.wait('@findSubmissions'); cy.contains( - '[data-cy="submission"]', + '[data-cy="row"]', 'YouTube to crack down on inappropriate content masked as kids’ cartoons' ).should('exist'); - cy.get('[data-cy="submission"] [data-cy="review-button"]').click(); - - const expectedValues = { - _id: '6272f2218933c7a9b512e13b', - text: 'Something', - submitters: 'Something', - authors: 'Valentina Palladino', - incident_date: '2021-09-21', - date_published: '2017-11-10', - image_url: - 'https://cdn.arstechnica.net/wp-content/uploads/2017/11/Screen-Shot-2017-11-10-at-9.25.47-AM-760x380.png', - incident_ids: [1], - url: `https://www.arstechnica.com/gadgets/2017/11/youtube-to-crack-down-on-inappropriate-content-masked-as-kids-cartoons/`, - source_domain: 'arstechnica.com', - language: 'en', - editor_notes: 'Here are some notes', - }; - - for (let key in expectedValues) { - cy.get(`[data-cy="${key}"]`) - .contains( - isArray(expectedValues[key]) ? arrayToList(expectedValues[key]) : expectedValues[key] - ) - .should('exist'); - } - - cy.contains('Please review. Some data is missing.').should('not.exist'); }); it('Should show a toast on error when failing to reach parsing endpoint', () => { diff --git a/site/gatsby-site/cypress/fixtures/submissions/submitted.json b/site/gatsby-site/cypress/fixtures/submissions/submitted.json index f8099d7f21..0314b73166 100644 --- a/site/gatsby-site/cypress/fixtures/submissions/submitted.json +++ b/site/gatsby-site/cypress/fixtures/submissions/submitted.json @@ -15,6 +15,7 @@ "epoch_date_modified": 1686163744, "description": "By NEIL BEDI and KATHLEEN McGRORY\nTimes staff writers\nNov. 19, 2020\nThe Pasco Sheriff’s Office keeps a secret list of kids it thinks could “fall into a life of crime” based on factors like wheth", "image_url": "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", + "incident_title": "Submisssion 1 title", "incident_date": "2015-09-01", "incident_ids": [], "language": "en", @@ -119,6 +120,7 @@ "date_submitted": "2021-08-23", "epoch_date_modified": 1686163744, "image_url": "https://skylightcyber.com/2019/07/18/cylance-i-kill-you/cylance-i-kill-you-small.gif", + "incident_title": "Submission 3 title", "incident_date": "2019-07-18", "incident_ids": [], "language": "en", diff --git a/site/gatsby-site/i18n/locales/es/submitted.json b/site/gatsby-site/i18n/locales/es/submitted.json index 02caa7663f..025dd115dc 100644 --- a/site/gatsby-site/i18n/locales/es/submitted.json +++ b/site/gatsby-site/i18n/locales/es/submitted.json @@ -17,5 +17,25 @@ "Successfully promoted submission to Issue {{report_number}}": "Envío promocionado con éxito al Problema {{report_number}}", "Are you sure this is a new incident? This will create a permanent record with all the details you provided about the incident.": "¿Seguro que desea promocionar este Envío a un nuevo Incidente?", "Sure you want to promote this Submission and link it to Incident {{incident_id}}?": "¿Seguro que desea promocionar este Envío y vincularlo al Incidente {{incident_id}}?", - "Are you sure you want to delete “{{url}}”?": "¿Estás seguro de que quieres eliminar “{{url}}”?" + "Are you sure you want to delete “{{url}}”?": "¿Estás seguro de que quieres eliminar “{{url}}”?", + "Editing Submission": "Editando Envío", + "Pending Review": "Revisión Pendiente", + "Back to Submission List": "Volver a la lista de envíos", + "Editors": "Editores", + "Unassigned": "Sin asignar", + "Status": "Estado", + "In Review": "En Revisión", + "Add as new": "Agregar como nuevo", + "Accept": "Aceptar", + "Reject": "Rechazar", + "Promoting to incident": "Promoviendo a incidente", + "Promoting to issue": "Promoviendo a problema", + "Adding as incident": "Agregando como incidente", + "Adding as issue": "Agregando como problema", + "Are you sure you want to reject this submission? This will permanently delete the submission.": "¿Estás seguro de que quieres rechazar este envío? Esto eliminará permanentemente el envío.", + "Changes saved": "Cambios guardados", + "There was an error claiming this submission. Please try again.": "Hubo un error al reclamar este envío. Por favor, inténtelo de nuevo.", + "Claim": "Reclamar", + "Claiming...": "Reclamando...", + "Reviewing": "Revisando" } diff --git a/site/gatsby-site/i18n/locales/es/translation.json b/site/gatsby-site/i18n/locales/es/translation.json index d4d306a66b..c1535fb6af 100644 --- a/site/gatsby-site/i18n/locales/es/translation.json +++ b/site/gatsby-site/i18n/locales/es/translation.json @@ -265,6 +265,7 @@ "Subscribe to the AI Incident Briefing and get monthly incident round-ups along with occasional major database updates.": "Subscribe to the AI Incident Briefing and get monthly incident round-ups along with occasional major database updates.", "Check your inbox for the AI Incident Briefing, which includes incident round-ups along with occasional major database updates. You can manage your subscription status from the links in the email footer.": "Check your inbox for the AI Incident Briefing, which includes incident round-ups along with occasional major database updates. You can manage your subscription status from the links in the email footer.", "Image URL is invalid, using fallback image": "La URL de la imagen no es válida, se utilizará la imagen de respaldo", + "Issue": "Problema", "You have successfully create Incident {{newIncidentId}}. <4>View incident.": "Incidente {{newIncidentId}} creado exitosamente. <4>Ver Incidente.", "New Incident": "Nuevo Incidente", diff --git a/site/gatsby-site/i18n/locales/fr/submitted.json b/site/gatsby-site/i18n/locales/fr/submitted.json index 9966465bef..6e67305efb 100644 --- a/site/gatsby-site/i18n/locales/fr/submitted.json +++ b/site/gatsby-site/i18n/locales/fr/submitted.json @@ -17,5 +17,25 @@ "Successfully promoted submission to Issue {{report_number}}": "Soumission promue avec succès au problème {{report_number}}", "Are you sure this is a new incident? This will create a permanent record with all the details you provided about the incident.": "Êtes-vous sûr qu'il s'agit d'un nouvel incident? Cela créera un enregistrement permanent avec tous les détails que vous avez fournis sur l'incident.", "Sure you want to promote this Submission and link it to Incident {{incident_id}}?": "Voulez-vous vraiment promouvoir cette soumission et la lier à l'incident {{incident_id}}?", - "Are you sure you want to delete “{{url}}”?": "Voulez-vous vraiment effacer « {{url}} » ?" + "Are you sure you want to delete “{{url}}”?": "Voulez-vous vraiment effacer « {{url}} » ?", + "Editing Submission": "Édition de la soumission", + "Pending Review": "En attente de révision", + "Back to Submission List": "Retour à la liste des soumissions", + "Editors": "Éditeurs", + "Unassigned": "Non assigné", + "Status": "Statut", + "In Review": "En révision", + "Add as new": "Ajouter comme", + "Accept": "Accepter", + "Reject": "Rejeter", + "Promoting to incident": "Promouvoir en tant qu'incident", + "Promoting to issue": "Promouvoir en tant que problème", + "Adding as incident": "Ajout en tant qu'incident", + "Adding as issue": "Ajout en tant que problème", + "Are you sure you want to reject this submission? This will permanently delete the submission.": "Voulez-vous vraiment rejeter cette soumission? Cela supprimera définitivement la soumission.", + "Changes saved": "Modifications enregistrées", + "There was an error claiming this submission. Please try again.": "Une erreur s'est produite lors de la réclamation de cette soumission. Veuillez réessayer.", + "Claim": "Réclamer", + "Claiming...": "En cours...", + "Reviewing": "Révision" } diff --git a/site/gatsby-site/i18n/locales/fr/translation.json b/site/gatsby-site/i18n/locales/fr/translation.json index 9ece79754e..44590d5eae 100644 --- a/site/gatsby-site/i18n/locales/fr/translation.json +++ b/site/gatsby-site/i18n/locales/fr/translation.json @@ -254,6 +254,7 @@ "Subscribe to the AI Incident Briefing and get monthly incident round-ups along with occasional major database updates.": "Subscribe to the AI Incident Briefing and get monthly incident round-ups along with occasional major database updates.", "Check your inbox for the AI Incident Briefing, which includes incident round-ups along with occasional major database updates. You can manage your subscription status from the links in the email footer.": "Check your inbox for the AI Incident Briefing, which includes incident round-ups along with occasional major database updates. You can manage your subscription status from the links in the email footer.", "Image URL is invalid, using fallback image": "L'URL de l'image n'est pas valide, utilisation d'une image de remplacement", + "Issue": "Problème", "You have successfully create Incident {{newIncidentId}}. <4>View incident.": "Vous avez réussi à créer l'incident {{newIncidentId}}. <4>Voir l'incident.", "New Incident": "Nouvel Incident", "csetCharts": "La taxonomie CSET AI Harm pour AIID est la deuxième édition de la taxonomie CSET incident. Il caractérise les dommages, les entités et les technologies impliquées dans les incidents d'IA et les circonstances de leur apparition. Les graphiques ci-dessous montrent certains champs de la taxonomie CSET AI Harm pour AIID. Des détails sur chaque champ peuvent être trouvés <1>ici. Cependant, de brèves descriptions du champ sont fournies au-dessus de chaque graphique.", diff --git a/site/gatsby-site/src/components/Layout.js b/site/gatsby-site/src/components/Layout.js index a99191c972..1587123009 100644 --- a/site/gatsby-site/src/components/Layout.js +++ b/site/gatsby-site/src/components/Layout.js @@ -12,7 +12,7 @@ const Layout = ({ children, className, sidebarCollapsed = false, location }) => <>
-
+
{config.sidebar.title && ( diff --git a/site/gatsby-site/src/components/forms/SubmissionWizard/StepContainer.js b/site/gatsby-site/src/components/forms/SubmissionWizard/StepContainer.js index 0f99e07de9..2a172a993c 100644 --- a/site/gatsby-site/src/components/forms/SubmissionWizard/StepContainer.js +++ b/site/gatsby-site/src/components/forms/SubmissionWizard/StepContainer.js @@ -1,12 +1,17 @@ +import { Badge } from 'flowbite-react'; import React from 'react'; const StepContainer = (props) => { return (
-
{props.name}
- {props.children} +
+ {props.children} +
+ {props.name} +
+
); }; diff --git a/site/gatsby-site/src/components/forms/SubmissionWizard/StepOne.js b/site/gatsby-site/src/components/forms/SubmissionWizard/StepOne.js index a825b4bb8f..95d6d92ba4 100644 --- a/site/gatsby-site/src/components/forms/SubmissionWizard/StepOne.js +++ b/site/gatsby-site/src/components/forms/SubmissionWizard/StepOne.js @@ -80,7 +80,7 @@ const StepOne = (props) => { }, [props.data]); return ( - + {}} diff --git a/site/gatsby-site/src/components/forms/SubmissionWizard/StepThree.js b/site/gatsby-site/src/components/forms/SubmissionWizard/StepThree.js index b8784d2b3f..0570102e1e 100644 --- a/site/gatsby-site/src/components/forms/SubmissionWizard/StepThree.js +++ b/site/gatsby-site/src/components/forms/SubmissionWizard/StepThree.js @@ -117,7 +117,7 @@ const StepThree = (props) => { } return ( - + {}} diff --git a/site/gatsby-site/src/components/forms/SubmissionWizard/StepTwo.js b/site/gatsby-site/src/components/forms/SubmissionWizard/StepTwo.js index db7be99118..a54d1caff5 100644 --- a/site/gatsby-site/src/components/forms/SubmissionWizard/StepTwo.js +++ b/site/gatsby-site/src/components/forms/SubmissionWizard/StepTwo.js @@ -47,7 +47,7 @@ const StepTwo = (props) => { }, [props.data]); return ( - + {}} diff --git a/site/gatsby-site/src/components/submissions/SubmissionEdit.js b/site/gatsby-site/src/components/submissions/SubmissionEdit.js new file mode 100644 index 0000000000..b567a01075 --- /dev/null +++ b/site/gatsby-site/src/components/submissions/SubmissionEdit.js @@ -0,0 +1,196 @@ +import { Button } from 'flowbite-react'; +import { Link } from 'gatsby'; +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { Formik } from 'formik'; +import useToastContext, { SEVERITY } from 'hooks/useToast'; +import { useLazyQuery, useMutation, useQuery } from '@apollo/client'; +import { FIND_ENTITIES, UPSERT_ENTITY } from '../../graphql/entities'; +import { format, getUnixTime } from 'date-fns'; +import { FIND_SUBMISSION, UPDATE_SUBMISSION } from '../../graphql/submissions'; +import { schema } from './schemas'; +import { stripMarkdown } from 'utils/typography'; +import { processEntities } from 'utils/entities'; +import isArray from 'lodash/isArray'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import DefaultSkeleton from 'elements/Skeletons/Default'; +import { FIND_USERS_BY_ROLE } from '../../graphql/users'; +import SubmissionEditForm from './SubmissionEditForm'; + +const SubmissionEdit = ({ id }) => { + const { data: entitiesData } = useQuery(FIND_ENTITIES); + + const [findSubmission, { data, loading }] = useLazyQuery(FIND_SUBMISSION); + + const [updateSubmission] = useMutation(UPDATE_SUBMISSION); + + const [createEntityMutation] = useMutation(UPSERT_ENTITY); + + const { data: userData, loading: userLoading } = useQuery(FIND_USERS_BY_ROLE, { + variables: { role: 'editor' }, + }); + + const addToast = useToastContext(); + + useEffect(() => { + findSubmission({ variables: { query: { _id: id } } }); + }, [id]); + + const [saving, setSaving] = useState(false); + + const [submission, setSubmission] = useState(null); + + const { i18n } = useTranslation(['submitted']); + + useEffect(() => { + if (data?.submission) { + setSubmission({ + ...data.submission, + }); + } + }, [data]); + + const handleSubmit = async (values) => { + try { + const update = { ...values, __typename: undefined, _id: undefined }; + + const { entities } = entitiesData; + + update.deployers = await processEntities(entities, values.deployers, createEntityMutation); + + update.developers = await processEntities(entities, values.developers, createEntityMutation); + + update.harmed_parties = await processEntities( + entities, + values.harmed_parties, + createEntityMutation + ); + + if (update.nlp_similar_incidents) { + update.nlp_similar_incidents = update.nlp_similar_incidents.map((nlp) => { + return { ...nlp, __typename: undefined }; + }); + } + + if (update.incident_editors) { + const userIds = update.incident_editors.map((user) => user.userId); + + update.incident_editors = { link: userIds }; + } + + const now = new Date(); + + const updatedSubmission = { + ...update, + incident_id: update.incident_id === '' ? 0 : update.incident_id, + authors: !isArray(values.authors) + ? values.authors.split(',').map((s) => s.trim()) + : values.authors, + submitters: values.submitters + ? !isArray(values.submitters) + ? values.submitters.split(',').map((s) => s.trim()) + : values.submitters + : ['Anonymous'], + plain_text: await stripMarkdown(update.text), + date_modified: format(now, 'yyyy-MM-dd'), + epoch_date_modified: getUnixTime(now), + }; + + await updateSubmission({ + variables: { + query: { + _id: values._id, + }, + set: updatedSubmission, + }, + }); + + setSaving(false); + setSubmission(values); + } catch (e) { + addToast({ + message: `Error updating submission ${values._id}`, + severity: SEVERITY.danger, + error: e, + }); + } + }; + + return ( +
+
+

+ + Editing Submission + +

+
+ + {saving ? ( + <> + {' '} + + Saving changes... + + + ) : ( + <> + + + Changes saved + + + )} + + + + +
+
+ {loading ? ( + + ) : ( + <> + {!loading && submission && entitiesData?.entities && ( +
+ item.name || item), + deployers: + submission.deployers === null + ? [] + : submission.deployers.map((item) => item.name || item), + harmed_parties: + submission.harmed_parties === null + ? [] + : submission.harmed_parties.map((item) => item.name || item), + }} + > + + +
+ )} + + )} +
+ ); +}; + +export default SubmissionEdit; diff --git a/site/gatsby-site/src/components/submissions/SubmissionEditForm.js b/site/gatsby-site/src/components/submissions/SubmissionEditForm.js new file mode 100644 index 0000000000..b97aeab071 --- /dev/null +++ b/site/gatsby-site/src/components/submissions/SubmissionEditForm.js @@ -0,0 +1,520 @@ +import { Badge, Button, Card, Checkbox, Dropdown, Label, Select } from 'flowbite-react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import SubmissionForm from './SubmissionForm'; +import { useFormikContext } from 'formik'; +import RelatedIncidents from 'components/RelatedIncidents'; +import useToastContext, { SEVERITY } from 'hooks/useToast'; +import { useMutation } from '@apollo/client'; +import { DELETE_SUBMISSION, PROMOTE_SUBMISSION } from '../../graphql/submissions'; +import { incidentSchema, issueSchema, reportSchema } from './schemas'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faBarsProgress, + faCheck, + faPlusSquare, + faUser, + faXmark, +} from '@fortawesome/free-solid-svg-icons'; +import { debounce } from 'debounce'; +import { STATUS } from 'utils/submissions'; +import StepContainer from 'components/forms/SubmissionWizard/StepContainer'; +import { useUserContext } from 'contexts/userContext'; +import { UPSERT_SUBSCRIPTION } from '../../graphql/subscriptions'; +import { SUBSCRIPTION_TYPE } from 'utils/subscriptions'; +import isEmpty from 'lodash/isEmpty'; +import useLocalizePath from 'components/i18n/useLocalizePath'; +import { DropdownItem } from 'flowbite-react/lib/esm/components/Dropdown/DropdownItem'; + +const SubmissionEditForm = ({ handleSubmit, saving, setSaving, userLoading, userData }) => { + const [promoting, setPromoting] = useState(''); + + const [deleting, setDeleting] = useState(false); + + const { values, touched, setFieldValue, setFieldTouched } = useFormikContext(); + + const isNewIncident = values.incident_ids.length === 0; + + const [promoType, setPromoType] = useState('none'); + + const localizedPath = useLocalizePath(); + + const [subscribeToNewSubmissionPromotionMutation] = useMutation(UPSERT_SUBSCRIPTION); + + useEffect(() => { + if (!isEmpty(touched)) { + setSaving(true); + saveChanges(values); + } + }, [values, touched]); + + const saveChanges = useRef( + debounce(async (values) => { + await handleSubmit(values); + setSaving(false); + }, 1000) + ).current; + + const addToast = useToastContext(); + + const { i18n, t } = useTranslation(['submitted']); + + const [promoteSubmissionToReport] = useMutation(PROMOTE_SUBMISSION, { + fetchPolicy: 'network-only', + }); + + const [subscribeToNewReportsMutation] = useMutation(UPSERT_SUBSCRIPTION); + + const [deleteSubmission] = useMutation(DELETE_SUBMISSION, { + update: (cache, { data }) => { + // Apollo expects a `deleted` boolean field otherwise manual cache manipulation is needed + cache.evict({ + id: cache.identify({ + __typename: data.deleteOneSubmission.__typename, + id: data.deleteOneSubmission._id, + }), + }); + }, + }); + + const promoteSubmission = ({ submission, variables }) => + promoteSubmissionToReport({ + variables, + fetchPolicy: 'no-cache', + update: (cache) => { + cache.modify({ + fields: { + submissions(refs, { readField }) { + return refs.filter((s) => submission._id !== readField('_id', s)); + }, + }, + }); + }, + }); + + const { user, isRole } = useUserContext(); + + const isSubmitter = isRole('submitter'); + + const subscribeToNewReports = async (incident_id) => { + if (user) { + await subscribeToNewReportsMutation({ + variables: { + query: { + type: SUBSCRIPTION_TYPE.incident, + userId: { userId: user.id }, + incident_id: { incident_id: incident_id }, + }, + subscription: { + type: SUBSCRIPTION_TYPE.incident, + userId: { + link: user.id, + }, + incident_id: { + link: incident_id, + }, + }, + }, + }); + } + }; + + const validateSchema = async ({ submission, schema }) => { + try { + await schema.validate(submission); + } catch (e) { + const [error] = e.errors; + + addToast({ + message: t(error), + severity: SEVERITY.danger, + error: e, + }); + + return false; + } + + return true; + }; + + const promoteToIssue = useCallback(async () => { + if (!(await validateSchema({ submission: values, schema: issueSchema }))) { + return; + } + + if ( + !confirm( + t( + 'Are you sure this is a new issue? Any data entered that is associated with incident records will not be added' + ) + ) + ) { + return; + } + + setPromoting('issue'); + + const { + data: { + promoteSubmissionToReport: { report_number }, + }, + } = await promoteSubmission({ + submission: values, + variables: { + input: { + submission_id: values._id, + incident_ids: [], + is_incident_report: false, + }, + }, + }); + + addToast({ + message: ( + + Successfully promoted submission to Issue {{ report_number }} + + ), + severity: SEVERITY.success, + }); + + setPromoting(''); + setTimeout(() => { + window.location.href = localizedPath({ path: '/apps/submitted' }); + }, 1000); + }, [values]); + + const promoteToIncident = useCallback(async () => { + if (!(await validateSchema({ submission: values, schema: incidentSchema }))) { + return; + } + + if ( + !confirm( + t( + 'Are you sure this is a new incident? This will create a permanent record with all the details you provided about the incident.' + ) + ) + ) { + return; + } + + setPromoting('incident'); + + const { + data: { + promoteSubmissionToReport: { report_number, incident_ids }, + }, + } = await promoteSubmission({ + submission: values, + variables: { + input: { + submission_id: values._id, + incident_ids: [], + is_incident_report: true, + }, + }, + }); + + const incident_id = incident_ids[0]; + + await subscribeToNewReports(incident_id); + + if (values.user) { + await subscribeToNewSubmissionPromotionMutation({ + variables: { + query: { + type: SUBSCRIPTION_TYPE.submissionPromoted, + userId: { userId: values.user.userId }, + incident_id: { incident_id: incident_id }, + }, + subscription: { + type: SUBSCRIPTION_TYPE.submissionPromoted, + userId: { + link: values.user.userId, + }, + incident_id: { + link: incident_id, + }, + }, + }, + }); + } + + addToast({ + message: ( + + Successfully promoted submission to Incident {{ incident_id }} and Report{' '} + {{ report_number }} + + ), + severity: SEVERITY.success, + }); + + setPromoting(''); + setTimeout(() => { + window.location.href = localizedPath({ path: '/apps/submitted' }); + }, 1000); + }, [values]); + + const promoteToReport = useCallback(async () => { + if (!(await validateSchema({ submission: values, schema: reportSchema }))) { + return; + } + + if ( + !confirm( + t('Sure you want to promote this Submission and link it to Incident {{ incident_id }}?', { + incident_id: values.incident_id, + }) + ) + ) { + return; + } + + setPromoting('incident'); + + const { + data: { + promoteSubmissionToReport: { report_number, incident_ids }, + }, + } = await promoteSubmission({ + submission: values, + variables: { + input: { + submission_id: values._id, + incident_ids: values.incident_ids, + is_incident_report: true, + }, + }, + }); + + for (const incident_id of incident_ids) { + await subscribeToNewReports(incident_id); + + addToast({ + message: ( + + Successfully promoted submission to Incident {{ incident_id }} and Report{' '} + {{ report_number }} + + ), + severity: SEVERITY.success, + }); + } + + setPromoting(''); + setTimeout(() => { + window.location.href = localizedPath({ path: '/apps/submitted' }); + }, 1000); + }, [values]); + + const rejectReport = async () => { + await deleteSubmission({ variables: { _id: values._id } }); + }; + + const promote = () => { + if (promoType === 'incident') { + promoteToIncident(); + } else if (promoType === 'issue') { + promoteToIssue(); + } + }; + + const reject = async () => { + if ( + !confirm( + t( + 'Are you sure you want to reject this submission? This will permanently delete the submission.' + ) + ) + ) { + return; + } + setDeleting(true); + await rejectReport(); + setDeleting(false); + setTimeout(() => { + window.location.href = localizedPath({ path: '/apps/submitted' }); + }, 1000); + }; + + const [selectedOptions, setSelectedOptions] = useState(values.incident_editors || []); + + const handleSelect = (checked, userId) => { + let userInfo = userData.users.find((user) => user.userId === userId); + + userInfo = { + userId: userInfo.userId, + first_name: userInfo.first_name, + last_name: userInfo.last_name, + }; + let selectedOptions = [...values.incident_editors]; + + if (checked) { + selectedOptions = [...selectedOptions, userInfo]; + } else { + selectedOptions = selectedOptions.filter((option) => option.userId !== userId); + } + setSelectedOptions(selectedOptions); + + setFieldValue('incident_editors', selectedOptions); + setFieldTouched('incident_editors', true); + }; + + return ( + <> + + + + {values.status ? STATUS[values.status].text : STATUS.pendingReview.text} + + + + + +
+
+
+ +
+ {!userLoading && ( + + {userData.users.map((user) => { + const isChecked = + selectedOptions.findIndex((editor) => editor.userId === user.userId) > -1; + + return ( + +
+ handleSelect(ev.target.checked, user.userId)} + /> + +
+
+ ); + })} +
+ )} +
+
+ +
+ + +
+
+ + {!isNewIncident && ( +
+ +
+ )} +
+ + + + + +
+ {promoting !== '' && ( + + Promoting to {promoting} + + )} +
+
+ + ); +}; + +export default SubmissionEditForm; diff --git a/site/gatsby-site/src/components/submissions/SubmissionForm.js b/site/gatsby-site/src/components/submissions/SubmissionForm.js index 2c5db7c3ae..cde2395098 100644 --- a/site/gatsby-site/src/components/submissions/SubmissionForm.js +++ b/site/gatsby-site/src/components/submissions/SubmissionForm.js @@ -36,9 +36,8 @@ import { import FlowbiteSearchInput from 'components/forms/FlowbiteSearchInput'; import { Select } from 'flowbite-react'; import IncidentsField from 'components/incidents/IncidentsField'; -import UsersInputGroup from 'components/forms/UsersInputGroup'; -const SubmissionForm = () => { +const SubmissionForm = ({ onChange = null }) => { const { values, errors, @@ -148,7 +147,7 @@ const SubmissionForm = () => { return (
-
+
{ className="mt-3" {...TextInputGroupProps} /> - { - const { data, loading } = useQuery(FIND_SUBMISSIONS); +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { Badge, Button, Select } from 'flowbite-react'; +import { useUserContext } from 'contexts/userContext'; +import { + useBlockLayout, + useFilters, + usePagination, + useResizeColumns, + useSortBy, + useTable, +} from 'react-table'; +import Table, { + DefaultColumnFilter, + DefaultColumnHeader, + SelectColumnFilter, + SelectDatePickerFilter, +} from 'components/ui/Table'; +import { STATUS } from 'utils/submissions'; +import { useMutation } from '@apollo/client'; +import { UPDATE_SUBMISSION } from '../../graphql/submissions'; +import useToastContext, { SEVERITY } from 'hooks/useToast'; - return ( - -

- - The following incident reports have been submitted by - users and are pending review by editors. Only editors may promote these records to - incident reports in the database. - -

- - {loading && } - {data?.submissions - .map((submission) => ({ ...submission, __typename: undefined })) - .sort( - (a, b) => new Date(a.date_submitted).getTime() - new Date(b.date_submitted).getTime() - ) - .map((submission) => ( -
- +const SubmissionList = ({ data }) => { + const { t } = useTranslation(); + + const { isLoggedIn, isRole, user } = useUserContext(); + + const [tableData, setTableData] = useState([]); + + const [claiming, setClaiming] = useState({ submissionId: null, value: false }); + + const [reviewing, setReviewing] = useState({ submissionId: null, value: false }); + + const [updateSubmission] = useMutation(UPDATE_SUBMISSION); + + const addToast = useToastContext(); + + useEffect(() => { + if (data) { + setTableData(data.submissions); + } + }, [data]); + + const claimSubmission = async (submissionId) => { + setClaiming({ submissionId, value: true }); + try { + const submission = data.submissions.find((submission) => submission._id === submissionId); + + const incidentEditors = [...submission.incident_editors]; + + const isAlreadyEditor = submission.incident_editors.find( + (editor) => editor.userId === user.customData.userId + ); + + if (!isAlreadyEditor) { + incidentEditors.push(user.customData.userId); + + await updateSubmission({ + variables: { + query: { + _id: submissionId, + }, + set: { incident_editors: { link: incidentEditors } }, + }, + }); + } + + setClaiming({ submissionId: null, value: false }); + } catch (error) { + addToast({ + message: t(`There was an error claiming this submission. Please try again.`), + severity: SEVERITY.danger, + }); + + setClaiming({ submissionId: null, value: false }); + } + }; + + const defaultColumn = React.useMemo( + () => ({ + Filter: DefaultColumnFilter, + Header: DefaultColumnHeader, + }), + [] + ); + + function SelectEditorsColumnFilter({ + column: { filterValue = [], setFilter, preFilteredRows, id }, + }) { + let options; + + options = React.useMemo(() => { + let options = []; + + preFilteredRows.forEach((row) => { + if (row.values[id]) { + let editors = row.values[id] + .filter((editor) => { + return editor.first_name && editor.last_name; + }) + .reduce((acc, editor) => { + const name = `${editor.first_name} ${editor.last_name}`; + + if (!options.find((e) => e === name)) { + acc.push(name); + } + return acc; + }, []); + + options = options.concat(editors); + } + }); + return options; + }, [id, preFilteredRows]); + + return ( + + ); + } + + function SelectStatusFilter({ column: { filterValue = [], setFilter, preFilteredRows, id } }) { + let options; + + options = React.useMemo(() => { + const options = []; + + Object.values(STATUS).forEach((status) => { + options.push(status); + }); + return options; + }, [id, preFilteredRows]); + + return ( + + ); + } + + const [dateFilter, setDateFilter] = useState('incident_date'); + + const [dateValues, setDateValues] = useState([]); + + function SelectDatesColumnFilter({ column }) { + return ( +
+ +
+ { + 0 ? dateValues[0] : null} + endDate={dateValues.length > 1 ? dateValues[1] : null} + setDates={(vals) => { + setDateValues(vals); + }} + /> + } +
+
+ ); + } + + const columns = React.useMemo(() => { + const columns = [ + { + className: 'min-w-[300px]', + title: t('Title'), + accessor: 'title', + width: 300, + }, + { + className: 'min-w-[150px]', + title: t('Submitters'), + accessor: 'submitters', + width: 150, + Filter: SelectColumnFilter, + Cell: ({ row: { values } }) => { + return ( +
+ {values.submitters.map((submitter, index) => { + return ( + + {submitter} + + ); + })}
- ))} - - + ); + }, + }, + { + className: 'min-w-[220px]', + width: 220, + title: t('Dates'), + accessor: 'incident_date', + Filter: SelectDatesColumnFilter, + sortType: (rowA, rowB) => { + if ( + rowA.original[dateFilter] && + rowA.original[dateFilter] !== '' && + rowB.original[dateFilter] && + rowB.original[dateFilter] !== '' + ) { + const dateRowA = new Date(rowA.original[dateFilter]); + + const dateRowB = new Date(rowB.original[dateFilter]); + + if (dateRowA > dateRowB) { + return 1; + } + + if (dateRowA < dateRowB) { + return -1; + } + + return 0; + } + }, + filter: (rows, _field, value) => + rows.filter((row) => { + let fields = [dateFilter]; + + const matchingFields = fields.filter((field) => { + const rowDate = Date.parse(row.original[field]); + + return value[0] <= rowDate && rowDate <= value[1]; + }); + + return matchingFields.length > 0; + }), + Cell: ({ row }) => { + const values = row.values; + + const dateSubmitted = row.original.date_submitted; + + const datePublished = row.original.date_published; + + return ( +
+ {values.incident_date && ( + + {`Inc: ${values.incident_date}`} + + )} + {dateSubmitted && ( + + {`Sub: ${dateSubmitted}`} + + )} + {datePublished && ( + + {`Pub: ${datePublished}`} + + )} +
+ ); + }, + }, + { + title: t('Editors'), + accessor: 'incident_editors', + className: 'min-w-[150px]', + width: 150, + Filter: SelectEditorsColumnFilter, + filter: (rows, [field], value) => + rows.filter((row) => { + let rowValue = row.values[field]; + + if (value === 'unassigned') return rowValue.length === 0; + + const results = rowValue.filter((editor) => { + const fullName = `${editor.first_name} ${editor.last_name}`; + + return fullName.toLowerCase().includes(value.toLowerCase()); + }); + + return results.length > 0; + }), + Cell: ({ row: { values } }) => { + const editors = values.incident_editors; + + if (!editors || editors.length <= 0) return <>; + + return ( +
+ {editors.map((editor) => { + const firstName = editor.first_name || ''; + + const lastName = editor.last_name || ''; + + const fullName = `${firstName} ${lastName}`; + + return ( +
+ {fullName} +
+ ); + })} +
+ ); + }, + }, + { + title: t('Status'), + className: 'min-w-[200px]', + accessor: 'status', + width: 200, + Filter: SelectStatusFilter, + filter: (rows, [field], value) => + rows.filter((row) => { + let rowValue = row.values[field]; + + if (!rowValue) { + rowValue = STATUS.pendingReview.name; + } + return rowValue === value; + }), + Cell: ({ row: { values } }) => { + let color = + STATUS[values.status]?.color || + 'bg-orange-100 text-orange-800 dark:bg-orange-200 dark:text-orange-900'; + + return ( +
+ + {STATUS[values.status]?.text || STATUS.pendingReview.text} + +
+ ); + }, + }, + ]; + + if (isRole('incident_editor')) { + columns.push({ + title: t('Actions'), + accessor: '_id', + className: 'min-w-[200px]', + width: 'auto', + disableFilters: true, + disableSortBy: true, + disableResizing: true, + Cell: ({ row: { values } }) => ( +
+ + {!values.editor && ( + + )} +
+ ), + }); + } + + return columns; + }, [isLoggedIn, claiming, reviewing, dateFilter]); + + const table = useTable( + { + columns, + data: tableData, + defaultColumn, + }, + useFilters, + useSortBy, + usePagination, + useBlockLayout, + useResizeColumns + ); + + const setSubmissionStatus = async (submission) => { + if (submission.status !== STATUS.inReview.name) { + setReviewing({ submissionId: submission._id, value: true }); + try { + await updateSubmission({ + variables: { + query: { + _id: submission._id, + }, + set: { status: STATUS.inReview.name }, + }, + }); + setReviewing({ submissionId: submission._id, value: false }); + } catch (error) { + addToast({ + message: t(`There was an error updating this submission. Please try again.`), + severity: SEVERITY.danger, + }); + setReviewing({ submissionId: submission._id, value: false }); + } + } + }; + + return ( +
+ + ); }; diff --git a/site/gatsby-site/src/components/submissions/SubmissionListWrapper.js b/site/gatsby-site/src/components/submissions/SubmissionListWrapper.js new file mode 100644 index 0000000000..e19ce8e299 --- /dev/null +++ b/site/gatsby-site/src/components/submissions/SubmissionListWrapper.js @@ -0,0 +1,27 @@ +import React from 'react'; +import Link from '../ui/Link'; +import { FIND_SUBMISSIONS } from '../../graphql/submissions'; +import { useQuery } from '@apollo/client'; +import { Trans } from 'react-i18next'; +import ListSkeleton from 'elements/Skeletons/List'; +import SubmissionList from './SubmissionList'; + +const SubmissionListWrapper = () => { + const { data, loading } = useQuery(FIND_SUBMISSIONS); + + return ( + <> +

+ + The following incident reports have been submitted by + users and are pending review by editors. Only editors may promote these records to + incident reports in the database. + +

+ {loading && } + {data && } + + ); +}; + +export default SubmissionListWrapper; diff --git a/site/gatsby-site/src/components/submissions/schemas.js b/site/gatsby-site/src/components/submissions/schemas.js index c9cac47939..ab4eec1ece 100644 --- a/site/gatsby-site/src/components/submissions/schemas.js +++ b/site/gatsby-site/src/components/submissions/schemas.js @@ -1,6 +1,11 @@ import * as yup from 'yup'; import { dateRegExp, isPastDate } from '../../utils/date'; +const incident_title = yup + .string() + .min(6, '*Title must have at least 6 characters') + .max(500, "*Titles can't be longer than 500 characters"); + const developers = yup.array( yup .string() @@ -110,6 +115,7 @@ export const schema = yup.object().shape({ }); export const incidentSchema = schema.shape({ + incident_title: incident_title.required('*Incident Title is required.'), developers: developers.required(), deployers: deployers.required(), harmed_parties: harmed_parties.required(), diff --git a/site/gatsby-site/src/components/ui/Table.js b/site/gatsby-site/src/components/ui/Table.js index 7cd6036405..b40273b549 100644 --- a/site/gatsby-site/src/components/ui/Table.js +++ b/site/gatsby-site/src/components/ui/Table.js @@ -71,6 +71,9 @@ export function DefaultDateCell({ cell }) { export function SelectDatePickerFilter({ column: { filterValue = [], preFilteredRows, setFilter, id }, + startDate = null, + endDate = null, + setDates = null, }) { const [min, max] = React.useMemo(() => { let min = new Date(preFilteredRows[0]?.values[id] ?? '1970-01-01').getTime(); @@ -95,32 +98,56 @@ export function SelectDatePickerFilter({ picker.startDate.format('MM/DD/YYYY') + ' - ' + picker.endDate.format('MM/DD/YYYY') ); setFilter([picker.startDate.valueOf(), picker.endDate.valueOf()]); + + if (setDates) { + setDates([picker.startDate.valueOf(), picker.endDate.valueOf()]); + } }; const handleCancel = (event, picker) => { picker.element.val(''); setFilter([min, max]); + if (setDates) { + setDates([]); + } + }; + + let initialSettings = { + showDropdowns: true, + autoUpdateInput: false, + locale: { + cancelLabel: 'Clear', + }, }; + let defaultValue = ''; + + if (startDate && endDate) { + const formatedStartDate = format(new Date(startDate), 'MM/dd/yyyy'); + + const formatedEndDate = format(new Date(endDate), 'MM/dd/yyyy'); + + defaultValue = `${formatedStartDate} - ${formatedEndDate}`; + initialSettings = { + ...initialSettings, + startDate: formatedStartDate, + endDate: formatedEndDate, + }; + } + return (
@@ -231,15 +258,33 @@ export default function Table({
{headerGroups.map((headerGroup) => ( - {headerGroup.headers.map((column) => ( - - ))} + {headerGroup.headers.map((column) => { + let headerProps = column.getHeaderProps(); + + const style = { ...headerProps.style }; + + if (column.width) { + style.width = Number.isInteger(column.width) + ? `${column.width}px` + : column.width; // Allows auto value + } + headerProps = { ...headerProps, style }; + return ( + + ); + })} ))} @@ -257,10 +302,23 @@ export default function Table({ data-cy={`row`} > {row.cells.map((cell) => { + const cellOriginalWidth = cell.column?.originalWidth; + + let cellProps = cell.getCellProps(); + + if (cellOriginalWidth) { + cellProps = { + ...cellProps, + style: { + ...cellProps.style, + minWidth: `${cellOriginalWidth}px`, //Doesn't allow resizing under the min-width + }, + }; + } return (
- {column.render('Header')} - + {column.render('Header')} + {column.getResizerProps && ( +
+ )} +
{cell.render('Cell')} diff --git a/site/gatsby-site/src/elements/ProgessCircle/index.js b/site/gatsby-site/src/elements/ProgessCircle/index.js new file mode 100644 index 0000000000..70db615206 --- /dev/null +++ b/site/gatsby-site/src/elements/ProgessCircle/index.js @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react'; + +const ProgressCircle = ({ + percentage, + size = 50, + strokeWidth = 5, + className = '', + color = null, +}) => { + const [offset, setOffset] = useState(0); + + useEffect(() => { + const progress = percentage / 100; + + const circumference = 2 * Math.PI * (size / 2 - strokeWidth / 2); + + setOffset(circumference * (1 - progress)); + }, [size, strokeWidth, percentage]); + + return ( + + + + + {percentage}% + + + ); +}; + +export default ProgressCircle; diff --git a/site/gatsby-site/src/graphql/submissions.js b/site/gatsby-site/src/graphql/submissions.js index 672a60649c..731f84671e 100644 --- a/site/gatsby-site/src/graphql/submissions.js +++ b/site/gatsby-site/src/graphql/submissions.js @@ -55,6 +55,7 @@ export const FIND_SUBMISSIONS = gql` entity_id name } + status user { userId } @@ -108,6 +109,7 @@ export const FIND_SUBMISSION = gql` } editor_similar_incidents editor_dissimilar_incidents + status } } `; diff --git a/site/gatsby-site/src/graphql/users.js b/site/gatsby-site/src/graphql/users.js index e25caf6db0..f276fec1a0 100644 --- a/site/gatsby-site/src/graphql/users.js +++ b/site/gatsby-site/src/graphql/users.js @@ -39,6 +39,23 @@ export const FIND_USER = gql` } `; +export const FIND_USERS_BY_ROLE = gql` + query FindUsersByRole($role: String!) { + users(query: { roles_in: [$role] }) { + roles + userId + first_name + last_name + adminData { + email + disabled + creationDate + lastAuthenticationDate + } + } + } +`; + export const UPDATE_USER_ROLES = gql` mutation UpdateUserRoles($roles: [String]!, $userId: String) { updateOneUser(query: { userId: $userId }, set: { roles: $roles }) { diff --git a/site/gatsby-site/src/pages/apps/submitted.js b/site/gatsby-site/src/pages/apps/submitted.js index 4663c148e9..77ca81d756 100644 --- a/site/gatsby-site/src/pages/apps/submitted.js +++ b/site/gatsby-site/src/pages/apps/submitted.js @@ -4,13 +4,19 @@ import { ObjectId } from 'bson'; import { useMutation, useQuery } from '@apollo/client'; import { DELETE_QUICKADD, FIND_QUICKADD } from '../../graphql/quickadd.js'; import { useUserContext } from '../../contexts/userContext'; -import SubmissionList from '../../components/submissions/SubmissionList'; +import SubmissionListWrapper from '../../components/submissions/SubmissionListWrapper'; import useToastContext, { SEVERITY } from '../../hooks/useToast'; import { Trans, useTranslation } from 'react-i18next'; import ListSkeleton from 'elements/Skeletons/List'; import { Badge, Button, ListGroup } from 'flowbite-react'; +import { useQueryParam } from 'use-query-params'; +import SubmissionEdit from 'components/submissions/SubmissionEdit'; const SubmittedIncidentsPage = ({ ...props }) => { + const [id] = useQueryParam('editSubmission'); + + const [pageLoading, setPageLoading] = useState(true); + const { isRole } = useUserContext(); const isAdmin = isRole('admin'); @@ -27,8 +33,12 @@ const SubmittedIncidentsPage = ({ ...props }) => { // Respond to a successful fetch of the quickadd data useEffect(() => { + if (id) { + setPageLoading(false); + } if (!loading && !error && data) { setQuickAdds(data['quickadds']); + setPageLoading(false); } else if (!loading && error) { addToast({ message: ( @@ -39,8 +49,9 @@ const SubmittedIncidentsPage = ({ ...props }) => { severity: SEVERITY.danger, error, }); + setPageLoading(false); } - }, [loading, data, error]); + }, [id, loading, data, error]); const submitDeleteQuickAdd = async (id) => { const bsonID = new ObjectId(id); @@ -80,77 +91,102 @@ const SubmittedIncidentsPage = ({ ...props }) => { return ( <> - {t('Submitted Incident Report List')} + {id ? ( + {t('Edit submission')} + ) : ( + {t('Submitted Incident Report List')} + )} -
-

- Submitted Incident Report List -

-
-
- -
-

- Quick Add URLs -

-

- - These reports were added anonymously by users in the Quick Add form on the landing - page - -

- - {sortedQuickAdds.length < 1 && } - {sortedQuickAdds.map(({ _id, url, date_submitted }) => ( -
-
-
- -
-
- {' '} - - {url} - -
-
- Sub: {date_submitted} -
-
+
+
+ +
+
+ {' '} + + {url} + +
+
+ Sub: {date_submitted} +
+
+
+
+ ))} +
- ))} - -
- + + )} + + )} ); }; diff --git a/site/gatsby-site/src/tailwind.css b/site/gatsby-site/src/tailwind.css index 6e43becad5..5d21487467 100644 --- a/site/gatsby-site/src/tailwind.css +++ b/site/gatsby-site/src/tailwind.css @@ -764,4 +764,14 @@ .latest-reports-carousel *[data-testid="carousel-indicator"] { @apply hidden !important; } + + .editors-dropdown, + .editors-dropdown *[data-testid="flowbite-tooltip-target"], + .editors-dropdown *[data-testid="flowbite-tooltip-target"] button { + @apply w-full !important; + } + + .editors-dropdown *[data-testid="flowbite-tooltip-target"] button span { + @apply w-full justify-between !important; + } } diff --git a/site/gatsby-site/src/utils/cloudinary.js b/site/gatsby-site/src/utils/cloudinary.js index 3fb7b785f4..3a4212d512 100644 --- a/site/gatsby-site/src/utils/cloudinary.js +++ b/site/gatsby-site/src/utils/cloudinary.js @@ -91,6 +91,9 @@ const Image = ({ cldImg={image} plugins={plugins} style={style} + onError={() => { + setLoadFailed(true); + }} /> ); diff --git a/site/gatsby-site/src/utils/submissions.js b/site/gatsby-site/src/utils/submissions.js new file mode 100644 index 0000000000..1823fd0972 --- /dev/null +++ b/site/gatsby-site/src/utils/submissions.js @@ -0,0 +1,22 @@ +const isEmpty = require('lodash/isEmpty'); + +const filter = require('lodash/filter'); + +module.exports.STATUS = { + inReview: { + name: 'inReview', + text: 'In Review', + color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-200 dark:text-yellow-900', + }, + pendingReview: { + name: 'pendingReview', + text: 'Pending Review', + color: 'bg-orange-100 text-orange-800 dark:bg-orange-200 dark:text-orange-900', + }, +}; + +module.exports.getRowCompletionStatus = (properties) => { + const nonEmptyCount = filter(properties, (value) => !isEmpty(value)).length; + + return Math.ceil((nonEmptyCount / properties.length) * 100); +}; diff --git a/site/gatsby-site/tailwind.config.js b/site/gatsby-site/tailwind.config.js index f7b099bf99..009d6f2859 100644 --- a/site/gatsby-site/tailwind.config.js +++ b/site/gatsby-site/tailwind.config.js @@ -18,6 +18,14 @@ let safelist = [ 'tw-btn-link', 'bg-amber-400', 'tw-toast', + 'bg-orange-100', + 'text-orange-800', + 'dark:bg-orange-200', + 'dark:text-orange-900', + 'bg-yellow-100', + 'text-yellow-800', + 'dark:bg-yellow-200', + 'dark:text-yellow-900', ]; // Whitelisting level options from ListItem component diff --git a/site/realm/data_sources/mongodb-atlas/aiidprod/submissions/schema.json b/site/realm/data_sources/mongodb-atlas/aiidprod/submissions/schema.json index 6974bb563d..25c0c79fea 100644 --- a/site/realm/data_sources/mongodb-atlas/aiidprod/submissions/schema.json +++ b/site/realm/data_sources/mongodb-atlas/aiidprod/submissions/schema.json @@ -144,6 +144,9 @@ }, "user": { "bsonType": "string" + }, + "status": { + "bsonType": "string" } }, "required": [