diff --git a/site/gatsby-site/i18n/locales/es/submitted.json b/site/gatsby-site/i18n/locales/es/submitted.json index b06659ec2e..438168c58d 100644 --- a/site/gatsby-site/i18n/locales/es/submitted.json +++ b/site/gatsby-site/i18n/locales/es/submitted.json @@ -34,9 +34,33 @@ "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...", + "There was an error claiming this submission. Please try again.": "Hubo un error al asignar este envío. Por favor, inténtelo de nuevo.", + "Claim": "Asignarme", + "Claiming...": "Asignándome...", "Reviewing": "Revisando", - "No reports found": "No se encontraron informes" + "No reports found": "No se encontraron informes", + "Are you sure you want to claim these submissions? This will assign you as the editor on all the selected submissions.": "¿Estás seguro de que quieres asignarte estos envíos? Esto te asignará como editor en todos los envíos seleccionados.", + "Are you sure you want to unclaim these submissions? This will unassign you as the editor on all the selected submissions.": "¿Estás seguro de que quieres desasignarte estos envíos? Esto eliminará tu asignación como editor en todos los envíos seleccionados.", + "Are you sure you want to reject these submissions? This will permanently delete the submissions.": "¿Estás seguro de que quieres rechazar estos envíos? Esto eliminará permanentemente los envíos.", + "There was an error performing this action. Please try again.": "Hubo un error al realizar esta acción. Por favor, inténtelo de nuevo.", + "Select All": "Seleccionar todo", + "Unclaim": "Desasignar", + "Promote to Incident": "Promover a Incidente", + "Promote to Issue": "Promover a Problema", + "Submitters": "Envíos", + "Title": "Título", + "Date": "Fecha", + "Actions": "Acciones", + "There was an error updating this submission. Please try again.": "Hubo un error al actualizar este envío. Por favor, inténtelo de nuevo.", + "Bulk Actions": "Acciones en masa", + "Apply": "Aplicar", + "Successfully claimed {{count}} submissions": "{{count}} envíos asignados exitosamente ", + "Successfully unclaimed {{count}} submissions": "{{count}} envíos desasignados exitosamente", + "Successfully rejected {{count}} submissions": "{{count}} envíos rechazados exitosamente", + "Successfully claimed {{count}} submission": "{{count}} envío asignado exitosamente ", + "Successfully unclaimed {{count}} submission": "{{count}} envío desasignado exitosamente", + "Successfully rejected {{count}} submission": "{{count}} envío rechazado exitosamente", + "Review": "Revisar", + "Unclaiming...": "Desasignando..." + } diff --git a/site/gatsby-site/i18n/locales/fr/submitted.json b/site/gatsby-site/i18n/locales/fr/submitted.json index 41227141c5..2dfacbb070 100644 --- a/site/gatsby-site/i18n/locales/fr/submitted.json +++ b/site/gatsby-site/i18n/locales/fr/submitted.json @@ -34,9 +34,33 @@ "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...", + "There was an error claiming this submission. Please try again.": "Une erreur s'est produite lors de l'l'attribution de cette soumission. Veuillez réessayer.", + "Claim": "S'attribuer", + "Claiming...": "S'attribuant...", "Reviewing": "Révision", - "No reports found": "Aucun rapport trouvé" + "No reports found": "Aucun rapport trouvé", + "Are you sure you want to claim these submissions? This will assign you as the editor on all the selected submissions.": "Êtes-vous sûr de vouloir vous attribuer ces soumissions ? Cela vous assignera comme éditeur pour toutes les soumissions sélectionnées.", + "Are you sure you want to unclaim these submissions? This will unassign you as the editor on all the selected submissions.": "Êtes-vous sûr de vouloir vous désattribuer ces soumissions ? Cela vous retirera en tant qu'éditeur sur toutes les soumissions sélectionnées.", + "Are you sure you want to reject these submissions? This will permanently delete the submissions.": "Êtes-vous sûr de vouloir rejeter ces soumissions ? Cela supprimera définitivement les soumissions.", + "There was an error performing this action. Please try again.": "Une erreur s'est produite lors de cette action. Veuillez réessayer.", + "Select All": "Tout sélectionner", + "Unclaim": "Désattribuer", + "Promote to Incident": "Promouvoir en incident", + "Promote to Issue": "Promouvoir en problème", + "Submitters": "Soumissionnaires", + "Title": "Titre", + "Date": "Date", + "Actions": "Actions", + "There was an error updating this submission. Please try again.": "Une erreur s'est produite lors de la mise à jour de cette soumission. Veuillez réessayer.", + "Bulk Actions": "Actions groupées", + "Apply": "Appliquer", + "Successfully claimed {{count}} submission": "{{count}} soumission désattribuée avec succès", + "Successfully unclaimed {{count}} submission": "{{count}} soumission désassignée avec succès", + "Successfully rejected {{count}} submission": "{{count}} soumission rejetée avec succès", + "Successfully claimed {{count}} submissions": "{{count}} soumissions attribuées avec succès", + "Successfully unclaimed {{count}} submissions": "{{count}} soumissions désattribuée avec succès", + "Successfully rejected {{count}} submissions": "{{count}} soumissions rejetées avec succès", + "Review": "Examiner", + "Unclaiming...": "Désattribution en cours......" + } diff --git a/site/gatsby-site/i18n/locales/ja/submitted.json b/site/gatsby-site/i18n/locales/ja/submitted.json index 085910539f..c962fd081a 100644 --- a/site/gatsby-site/i18n/locales/ja/submitted.json +++ b/site/gatsby-site/i18n/locales/ja/submitted.json @@ -38,5 +38,28 @@ "Claim": "再投稿", "Claiming...": "再投稿中...", "Reviewing": "レビュー中", - "No reports found": "レポートが見つかりません" + "No reports found": "レポートが見つかりません", + "Are you sure you want to claim these submissions? This will assign you as the editor on all the selected submissions.": "これらの投稿を担当しますか?選択されたすべての投稿のエディタとして割り当てられます。", + "Are you sure you want to unclaim these submissions? This will unassign you as the editor on all the selected submissions.": "これらの投稿の担当を外しますか?選択されたすべての投稿のエディタとしての割り当てが解除されます。", + "Are you sure you want to reject these submissions? This will permanently delete the submissions.": "これらの投稿を却下しますか?投稿は完全に削除されます。", + "There was an error performing this action. Please try again.": "この操作の実行中にエラーが発生しました。もう一度お試しください。", + "Select All": "すべて選択", + "Unclaim": "担当を外す", + "Promote to Incident": "インシデントに更新", + "Promote to Issue": "イシューに更新", + "Submitters": "投稿者", + "Title": "タイトル", + "Date": "日付", + "Actions": "アクション", + "There was an error updating this submission. Please try again.": "この投稿の更新中にエラーが発生しました。もう一度お試しください。", + "Bulk Actions": "一括操作", + "Apply": "適用", + "Successfully claimed {{count}} submission": "{{count}} 投稿を成功裡に再投稿しました", + "Successfully unclaimed {{count}} submission": "{{count}} 投稿を成功裡に担当を外しました", + "Successfully rejected {{count}} submission": "{{count}} 投稿を成功裡に却下しました", + "Successfully claimed {{count}} submissions": "{{count}} 投稿を成功裡に再投稿しました", + "Successfully unclaimed {{count}} submissions": "{{count}} 投稿を成功裡に担当を外しました", + "Successfully rejected {{count}} submissions": "{{count}} 投稿を成功裡に却下しました", + "Review": "レビュー", + "Unclaiming...": "担当を外しています..." } diff --git a/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts b/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts index a521f6dc65..a0af6584b7 100644 --- a/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts +++ b/site/gatsby-site/playwright/e2e-full/apps/submitted.spec.ts @@ -700,4 +700,271 @@ test.describe('Submitted reports', () => { }) }); + + test('Should perform a bulk claim on all submissions', async ({ page, login }) => { + await init(); + + const [userId] = await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + await page.getByTestId("select-all-submissions").click({force: true}); + await page.getByTestId("select-all-submissions").check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select claim option + await page.getByTestId("bulk-action-select").selectOption('claim'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully claimed ${submissions.length} submissions`); + + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions(filter: { incident_editors: { EQ: "${userId}" } }) { + _id + incident_editors { + userId + } + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(submissions.length); + }); + + test('Should perform a bulk unclaim on all submissions', async ({ page, login }) => { + await init(); + + const [userId] = await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + await page.getByTestId("select-all-submissions").click({force: true}); + await page.getByTestId("select-all-submissions").check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select unclaim option + await page.getByTestId("bulk-action-select").selectOption('unclaim'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully unclaimed ${submissions.length} submissions`); + + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions(filter: { incident_editors: { EQ: "${userId}" } }) { + _id + incident_editors { + userId + } + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(0); + }); + + test('Should perform a bulk to reject all submissions', async ({ page, login }) => { + await init(); + + await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + await page.getByTestId("select-all-submissions").click({force: true}); + await page.getByTestId("select-all-submissions").check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select reject option + await page.getByTestId("bulk-action-select").selectOption('reject'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully rejected ${submissions.length} submissions`); + + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(0); + }); + + test('Should select and claim one submission', async ({ page, login }) => { + await init(); + + const [userId] = await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + const firstSubmission = submissions[0]; + await page.getByTestId(`select-submission-${firstSubmission._id}`).click({force: true}); + await page.getByTestId(`select-submission-${firstSubmission._id}`).check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select claim option + await page.getByTestId("bulk-action-select").selectOption('claim'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully claimed 1 submission`); + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions(filter: { incident_editors: { EQ: "${userId}" }, _id: { EQ: "${firstSubmission._id}" } }) { + _id + incident_editors { + userId + } + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(1); + }); + + test('Should select and unclaim one submission', async ({ page, login }) => { + await init(); + + const [userId] = await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + const firstSubmission = submissions[0]; + await page.getByTestId(`select-submission-${firstSubmission._id}`).click({force: true}); + await page.getByTestId(`select-submission-${firstSubmission._id}`).check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select unclaim option + await page.getByTestId("bulk-action-select").selectOption('unclaim'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully unclaimed 1 submission`); + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions(filter: { incident_editors: { EQ: "${userId}" }, _id: { EQ: "${firstSubmission._id}" } }) { + _id + incident_editors { + userId + } + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(0); + }); + + test('Should select and reject one submission', async ({ page, login }) => { + await init(); + + const [userId] = await login({ customData: { first_name: 'Test', last_name: 'User', roles: ['incident_editor'] } }); + + await page.goto(url); + + const { data: { submissions } } = await query({ + query: gql`{ + submissions { + _id + } + } + `, + }); + // Select all submissions + const firstSubmission = submissions[0]; + await page.getByTestId(`select-submission-${firstSubmission._id}`).click({force: true}); + await page.getByTestId(`select-submission-${firstSubmission._id}`).check({force: true}); + + page.on('dialog', dialog => dialog.accept()); + + // Select reject option + await page.getByTestId("bulk-action-select").selectOption('reject'); + + // Click on bulk action button + await page.getByTestId("bulk-action-button").click(); + + await expect(page.locator('[data-cy="toast"]').first()).toContainText(`Successfully rejected 1 submission`); + + const { data: { submissions: updatedSubmissions } } = await query({ + query: gql`{ + submissions(filter: { incident_editors: { EQ: "${userId}" }, _id: { EQ: "${firstSubmission._id}" } }) { + _id + incident_editors { + userId + } + } + } + `, + }); + + expect(updatedSubmissions.length).toBe(0); + }); }); \ No newline at end of file diff --git a/site/gatsby-site/playwright/seeds/aiidprod/submissions.ts b/site/gatsby-site/playwright/seeds/aiidprod/submissions.ts index ac65a8fda2..b549171390 100644 --- a/site/gatsby-site/playwright/seeds/aiidprod/submissions.ts +++ b/site/gatsby-site/playwright/seeds/aiidprod/submissions.ts @@ -2,33 +2,114 @@ import { ObjectId } from 'bson'; import { DBSubmission } from '../../../server/interfaces'; const submissions: DBSubmission[] = [ - { - _id: new ObjectId("6140e4b4b9b4f7b3b3b1b1b1"), - authors: ["Author 1", "Author 2"], - cloudinary_id: "sample_cloudinary_id", - date_downloaded: "2021-09-14", - date_modified: "2021-09-14T00:00:00.000Z", - date_published: "2021-09-14", - date_submitted: "2021-09-14T00:00:00.000Z", - deployers: ["entity-1"], - description: "Sample description", - developers: ["entity-2"], - harmed_parties: ["entity-3"], - incident_editors: ["editor1"], - image_url: "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", - language: "en", - source_domain: "example.com", - submitters: ["Submitter 1", "Submitter 2"], - tags: ["tag1", "tag2"], - text: "Sample text that must have at least 80 characters, so I will keep writing until I reach the minimum number of characters.", - title: "Sample title", - url: "http://example.com", - user: "6737a6e881955aa4905ccb04", - implicated_systems: ["entity-1"], - incident_title: "Incident title", - incident_date: "2021-09-14", - editor_notes: "This is an editor note", - }, + { + _id: new ObjectId("6140e4b4b9b4f7b3b3b1b1b1"), + authors: ["Author 1", "Author 2"], + cloudinary_id: "sample_cloudinary_id", + date_downloaded: "2021-09-14", + date_modified: "2021-09-14T00:00:00.000Z", + date_published: "2021-09-14", + date_submitted: "2021-09-14T00:00:00.000Z", + deployers: ["entity-1"], + description: "Sample description", + developers: ["entity-2"], + harmed_parties: ["entity-3"], + incident_editors: ["editor1"], + image_url: "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", + language: "en", + source_domain: "example.com", + submitters: ["Submitter 1", "Submitter 2"], + tags: ["tag1", "tag2"], + text: "Sample text that must have at least 80 characters, so I will keep writing until I reach the minimum number of characters.", + title: "Sample title", + url: "http://example.com", + user: "6737a6e881955aa4905ccb04", + implicated_systems: ["entity-1"], + incident_title: "Incident title", + incident_date: "2021-09-14", + editor_notes: "This is an editor note", + }, + { + _id: new ObjectId("6140e4b4b9b4f7b3b3b1b1b2"), + authors: ["Author 3", "Author 4"], + cloudinary_id: "sample_cloudinary_id", + date_downloaded: "2021-09-14", + date_modified: "2021-09-14T00:00:00.000Z", + date_published: "2021-09-14", + date_submitted: "2021-09-14T00:00:00.000Z", + deployers: ["entity-1"], + description: "Sample description 2", + developers: ["entity-2"], + harmed_parties: ["entity-3"], + incident_editors: ["editor1"], + image_url: "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", + language: "en", + source_domain: "example.com", + submitters: ["Submitter 1", "Submitter 2"], + tags: ["tag1", "tag2"], + text: "Sample text that must have at least 80 characters, so I will keep writing until I reach the minimum number of characters.", + title: "Sample title 2", + url: "http://example.com", + user: "6732f6z881832sd4905acb05", + implicated_systems: ["entity-1"], + incident_title: "Incident title", + incident_date: "2021-09-14", + editor_notes: "This is an editor note", + }, + { + _id: new ObjectId("6140e4b4b9b4f7b3b3b1b1b3"), + authors: ["Author 5", "Author 6"], + cloudinary_id: "sample_cloudinary_id", + date_downloaded: "2021-09-14", + date_modified: "2021-09-14T00:00:00.000Z", + date_published: "2021-09-14", + date_submitted: "2021-09-14T00:00:00.000Z", + deployers: ["entity-1"], + description: "Sample description 3", + developers: ["entity-2"], + harmed_parties: ["entity-3"], + incident_editors: ["editor1"], + image_url: "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", + language: "en", + source_domain: "example.com", + submitters: ["Submitter 1", "Submitter 2"], + tags: ["tag1", "tag2"], + text: "Sample text that must have at least 80 characters, so I will keep writing until I reach the minimum number of characters.", + title: "Sample title 3", + url: "http://example.com", + user: "6732f6z881832sd4905acb05", + implicated_systems: ["entity-1"], + incident_title: "Incident title", + incident_date: "2021-09-14", + editor_notes: "This is an editor note", + }, + { + _id: new ObjectId("6140e4b4b9b4f7b3b3b1b1b4"), + authors: ["Author 7", "Author 8"], + cloudinary_id: "sample_cloudinary_id", + date_downloaded: "2021-09-14", + date_modified: "2021-09-14T00:00:00.000Z", + date_published: "2021-09-14", + date_submitted: "2021-09-14T00:00:00.000Z", + deployers: ["entity-1"], + description: "Sample description 4", + developers: ["entity-2"], + harmed_parties: ["entity-3"], + incident_editors: ["editor1"], + image_url: "https://s3.amazonaws.com/ledejs/resized/s2020-pasco-ilp/600/nocco5.jpg", + language: "en", + source_domain: "example.com", + submitters: ["Submitter 1", "Submitter 2"], + tags: ["tag1", "tag2"], + text: "Sample text that must have at least 80 characters, so I will keep writing until I reach the minimum number of characters.", + title: "Sample title 4", + url: "http://example.com", + user: "6732f6z881832sd4905acb05", + implicated_systems: ["entity-1"], + incident_title: "Incident title", + incident_date: "2021-09-14", + editor_notes: "This is an editor note", + }, ] diff --git a/site/gatsby-site/playwright/seeds/auth/users.ts b/site/gatsby-site/playwright/seeds/auth/users.ts index 834245505c..544fc01f40 100644 --- a/site/gatsby-site/playwright/seeds/auth/users.ts +++ b/site/gatsby-site/playwright/seeds/auth/users.ts @@ -5,6 +5,11 @@ const users = [ _id: new ObjectId("6737a6e881955aa4905ccb04"), email: "test.user@incidentdatabase.ai", emailVerified: new Date("2024-11-15T21:41:04.245Z"), + }, + { + _id: new ObjectId("67a371b3fc0f0b924a91f636"), + email: "test.user.editor@incidentdatabase.ai", + emailVerified: new Date("2024-11-15T21:41:04.245Z"), } ] diff --git a/site/gatsby-site/playwright/seeds/customData/users.ts b/site/gatsby-site/playwright/seeds/customData/users.ts index a3daaf398a..eeff31cd0f 100644 --- a/site/gatsby-site/playwright/seeds/customData/users.ts +++ b/site/gatsby-site/playwright/seeds/customData/users.ts @@ -15,6 +15,13 @@ const users: DBUser[] = [ first_name: "John", last_name: "Doe" }, + { + _id: new ObjectId("619b47eb5eed5334edfa3ba9"), + userId: '67a371b3fc0f0b924a91f636', + first_name: "Test", + last_name: "User Editor", + roles: ["incident_editor"], + }, ] export default users; \ No newline at end of file diff --git a/site/gatsby-site/src/components/submissions/SubmissionList.js b/site/gatsby-site/src/components/submissions/SubmissionList.js index c4254c4b47..c1de478465 100644 --- a/site/gatsby-site/src/components/submissions/SubmissionList.js +++ b/site/gatsby-site/src/components/submissions/SubmissionList.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Badge, Button, Select } from 'flowbite-react'; +import { Badge, Button, Checkbox, Select, Spinner } from 'flowbite-react'; import { useUserContext } from 'contexts/UserContext'; import { useBlockLayout, @@ -18,11 +18,11 @@ import Table, { } from 'components/ui/Table'; import { STATUS } from 'utils/submissions'; import { useMutation } from '@apollo/client'; -import { UPDATE_SUBMISSION } from '../../graphql/submissions'; +import { DELETE_SUBMISSION, UPDATE_SUBMISSION } from '../../graphql/submissions'; import useToastContext, { SEVERITY } from 'hooks/useToast'; const SubmissionList = ({ data }) => { - const { t } = useTranslation(); + const { t } = useTranslation('submitted'); const { loading, isRole, user } = useUserContext(); @@ -34,8 +34,47 @@ const SubmissionList = ({ data }) => { const [updateSubmission] = useMutation(UPDATE_SUBMISSION); + 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 addToast = useToastContext(); + const [selectedRows, setSelectedRows] = useState([]); + + // Function to toggle individual row selection + const toggleRowSelection = (id) => { + setSelectedRows((prev) => { + if (prev.includes(id)) { + return prev.filter((item) => item !== id); + } + return [...prev, id]; + }); + }; + + // Function to toggle "Select All" + const toggleSelectAll = (isChecked) => { + const newSelection = []; + + if (isChecked) { + tableData.forEach((item) => { + newSelection.push(item._id); + }); + } + setSelectedRows(newSelection); + }; + + // Function to check if all rows are selected + const allSelected = selectedRows.length === tableData.length && tableData.length > 0; + useEffect(() => { if (data) { setTableData(data.submissions); @@ -175,8 +214,122 @@ const SubmissionList = ({ data }) => { ); } + const [selectedAction, setSelectedAction] = useState('claim'); + + const [performingAction, setPerformingAction] = useState(false); + + const bulkActions = async () => { + try { + if (selectedRows.length > 0) { + await bulkAction(selectedAction); + } + } catch (error) { + addToast({ + message: t(`There was an error performing this action. Please try again.`), + severity: SEVERITY.danger, + }); + } finally { + setPerformingAction(false); + setSelectedRows([]); + } + }; + + const bulkAction = async (selectedAction) => { + const actions = { + claim: { + confirmMessage: t( + 'Are you sure you want to claim these submissions? This will assign you as the editor on all the selected submissions.' + ), + execute: (submissionId) => claimSubmission(submissionId), + successMessage: t( + `Successfully claimed {{count}} submission${selectedRows.length === 1 ? '' : 's'}`, + { count: selectedRows.length } + ), + }, + unclaim: { + confirmMessage: t( + 'Are you sure you want to unclaim these submissions? This will unassign you as the editor on all the selected submissions.' + ), + execute: (submissionId) => unclaimSubmission(submissionId), + successMessage: t( + `Successfully unclaimed {{count}} submission${selectedRows.length === 1 ? '' : 's'}`, + { count: selectedRows.length } + ), + }, + reject: { + confirmMessage: t( + 'Are you sure you want to reject these submissions? This will permanently delete the submissions.' + ), + execute: (submissionId) => deleteSubmission({ variables: { _id: submissionId } }), + successMessage: t( + `Successfully rejected {{count}} submission${selectedRows.length === 1 ? '' : 's'}`, + { count: selectedRows.length } + ), + }, + }; + + const actionConfig = actions[selectedAction]; + + if (actionConfig) { + await executeBulkAction(actionConfig); + } + }; + + const executeBulkAction = async ({ confirmMessage, execute, successMessage }) => { + if (!confirm(confirmMessage)) { + return; + } + + setPerformingAction(true); + try { + const promises = selectedRows.map((submissionId) => execute(submissionId)); + + await Promise.all(promises); + + addToast({ + message: successMessage, + severity: SEVERITY.success, + }); + } catch (error) { + addToast({ + message: t(`There was an error performing this action. Please try again.`), + severity: SEVERITY.danger, + }); + } + }; + const columns = React.useMemo(() => { - const columns = [ + let columns = []; + + if (isRole('incident_editor')) { + columns.push({ + title: ( + { + return toggleSelectAll(e.target.checked); + }} + /> + ), + accessor: 'select', + className: 'min-w-[50px]', + width: 50, + disableFilters: true, + disableSortBy: true, + disableResizing: true, + Cell: ({ row }) => { + return ( + toggleRowSelection(row.original._id)} + data-testid={`select-submission-${row.original._id}`} + /> + ); + }, + }); + } + columns.push( { className: 'min-w-[300px]', title: t('Title'), @@ -340,13 +493,15 @@ const SubmissionList = ({ data }) => { return (
- {STATUS[values.status]?.text || STATUS.pendingReview.text} + + {STATUS[values.status]?.text || STATUS.pendingReview.text} +
); }, - }, - ]; + } + ); if (isRole('incident_editor')) { columns.push({ @@ -375,9 +530,9 @@ const SubmissionList = ({ data }) => { disabled={reviewing.value} > {reviewing.value && values._id === reviewing.submissionId ? ( - Reviewing... + Reviewing... ) : ( - Review + Review )} {!values.editor && ( @@ -392,17 +547,17 @@ const SubmissionList = ({ data }) => { {isAlreadyEditor ? ( <> {claiming.value && values._id === claiming.submissionId ? ( - Unclaiming... + Unclaiming... ) : ( - Unclaim + Unclaim )} ) : ( <> {claiming.value && values._id === claiming.submissionId ? ( - Claiming... + Claiming... ) : ( - Claim + Claim )} )} @@ -415,7 +570,7 @@ const SubmissionList = ({ data }) => { } return columns; - }, [loading, user, claiming, reviewing, dateFilter]); + }, [loading, user, claiming, reviewing, dateFilter, selectedRows, allSelected]); const [tableState, setTableState] = useState({ pageIndex: 0, filters: [], sortBy: [] }); @@ -534,7 +689,38 @@ const SubmissionList = ({ data }) => { }; return ( -
+
+ {performingAction && ( +
+ +
+ )} + {/* Actions */} + {selectedRows && selectedRows.length > 0 && ( +
+
+ + +
+
+ )}