diff --git a/cypress/e2e/tests/image.cy.ts b/cypress/e2e/tests/image.cy.ts index b13182e4a..7865b2a7d 100644 --- a/cypress/e2e/tests/image.cy.ts +++ b/cypress/e2e/tests/image.cy.ts @@ -2,6 +2,7 @@ import locators from "../../support/locators"; import claim from "../../fixtures/claim"; +import { today } from "../../utils/dateUtils"; describe("Create image claim", () => { beforeEach("login", () => cy.login()); @@ -15,7 +16,7 @@ describe("Create image claim", () => { .type(claim.imageTitle); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") diff --git a/cypress/e2e/tests/personality.cy.ts b/cypress/e2e/tests/personality.cy.ts index b69893d4b..084de10e4 100644 --- a/cypress/e2e/tests/personality.cy.ts +++ b/cypress/e2e/tests/personality.cy.ts @@ -3,6 +3,7 @@ import locators from "../../support/locators"; import claim from "../../fixtures/claim"; import personality from "../../fixtures/personality"; +import { today } from "../../utils/dateUtils"; describe("Create personality and claim", () => { beforeEach("login", () => cy.login()); @@ -47,7 +48,7 @@ describe("Create personality and claim", () => { .type(claim.content); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") @@ -78,7 +79,7 @@ describe("Create personality and claim", () => { .should("be.visible") .type(claim.imageTitle); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") .type(claim.source); diff --git a/cypress/e2e/tests/verificationRequest.cy.ts b/cypress/e2e/tests/verificationRequest.cy.ts new file mode 100644 index 000000000..9444078ee --- /dev/null +++ b/cypress/e2e/tests/verificationRequest.cy.ts @@ -0,0 +1,182 @@ + +/// + +import { fullVerificationRequest, regexVerificationRequestPage, updatedSource, minimumContent } from "../../fixtures/verificationRequest"; +import locators from "../../support/locators"; +import { Dayjs } from "dayjs" +import { getPastDay, today } from "../../utils/dateUtils"; + +describe("Test verification request", () => { + const getHashFromUrl = (interception) => interception.request.url.split("/").pop(); + + const openCreateVerificationRequestForm = () => { + cy.get(locators.floatButton.FLOAT_BUTTON).click(); + cy.get(locators.floatButton.ADD_VERIFICATION_REQUEST).click(); + cy.url().should("contain", "/verification-request/create"); + }; + + const selectPublicationDate = (date: Dayjs) => { + cy.get(locators.verificationRequest.FORM_PUBLICATION_DATE).click(); + cy.contains('[role="gridcell"]', date.format("D")).click(); + }; + + const saveVerificationRequest = () => { + cy.checkRecaptcha(); + cy.get(locators.verificationRequest.SAVE_BUTTON).click(); + }; + + const assertDetailFields = (fields: Array<[string, string]>) => { + fields.forEach(([selector, expected]) => { + cy.get(selector).should("be.visible").and("contain", expected); + }); + }; + + const goToVerificationRequest = (data_hash: string) => { + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + cy.visit(`/verification-request/${data_hash}`); + cy.wait("@getVerification"); + } + + describe("lifecycle verification request", () => { + beforeEach("login", () => cy.login()); + + it("should prevent submission when required fields are missing", () => { + openCreateVerificationRequestForm(); + saveVerificationRequest(); + cy.get(locators.verificationRequest.ERROR_VALIDATION_CONTENT).should("be.visible") + cy.get(locators.verificationRequest.ERROR_VALIDATION_PUBLICATION_DATE).should("be.visible") + cy.url().should("contain", "/verification-request/create"); + }); + + describe("Full verification request flow", () => { + let fullRequestHash: string; + + it("should create a verification request with all optional and mandatory fields", () => { + openCreateVerificationRequestForm(); + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + + cy.get(locators.verificationRequest.FORM_CONTENT).type(fullVerificationRequest.content); + selectPublicationDate(today); + cy.get(locators.verificationRequest.FORM_REPORT_TYPE).click(); + cy.contains(fullVerificationRequest.reportType).click(); + cy.get(locators.verificationRequest.FORM_IMPACT_AREA).type(fullVerificationRequest.impactArea, { delay: 200 }); + cy.contains(fullVerificationRequest.impactArea).click(); + + cy.get(locators.verificationRequest.FORM_HEARD_FROM).type(fullVerificationRequest.heardFrom); + cy.get(locators.verificationRequest.FORM_SOURCE).type(`https://${fullVerificationRequest.source}`); + cy.get(locators.verificationRequest.FORM_EMAIL).type(fullVerificationRequest.email); + saveVerificationRequest(); + + cy.wait("@getVerification").then((interception) => { + getHashFromUrl(interception) + fullRequestHash = getHashFromUrl(interception); + cy.log("Hash capturado:", fullRequestHash); + }); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_CONTENT, fullVerificationRequest.content], + [locators.verificationRequest.DETAIL_REPORT_TYPE, fullVerificationRequest.reportType], + [locators.verificationRequest.DETAIL_IMPACT_AREA, fullVerificationRequest.impactArea], + [locators.verificationRequest.DETAIL_SOURCE_CHANNEL, "Web"], + [locators.verificationRequest.DETAIL_HEARD_FROM, fullVerificationRequest.heardFrom], + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, today.format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_DATE, today.format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_0, fullVerificationRequest.source], + ]); + + it("should update an existing request by adding additional sources and modifying the publication date", () => { + goToVerificationRequest(fullRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ADD).click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_1).type(`https://${updatedSource}`); + selectPublicationDate(getPastDay(1)); + saveVerificationRequest(); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, getPastDay(1).format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_1, updatedSource], + ]) + }) + }) + + it("should manage topic tags by adding and removing them", () => { + goToVerificationRequest(fullRequestHash) + cy.intercept("PUT", "**/verification-request/*/topics").as("updateTopics"); + + cy.get(locators.verificationRequest.ADD_TOPIC_ICON).click(); + cy.get(locators.verificationRequest.TYPE_TOPIC_INPUT).type(fullVerificationRequest.topic, { delay: 200 }); + cy.contains(fullVerificationRequest.topic).click(); + cy.get(locators.verificationRequest.ADD_TOPIC_SUBMIT).click(); + + cy.wait("@updateTopics").then((interception) => { + expect(interception.response.statusCode).to.be.oneOf([200, 201]); + }); + + cy.contains(locators.verificationRequest.DETAIL_TOPIC_TAG, fullVerificationRequest.topic.toUpperCase()) + .should("be.visible") + .within(() => { + cy.get(locators.verificationRequest.REMOVE_TOPIC_ICON).click(); + }); + + + cy.contains(locators.verificationRequest.DETAIL_TOPIC_TAG, fullVerificationRequest.topic.toUpperCase()).should("not.exist"); + }) + + it("should discard unsaved changes when the edition form is canceled", () => { + goToVerificationRequest(fullRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ADD).click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_1).type(`https://${updatedSource}`); + selectPublicationDate(getPastDay(10)); + cy.get(locators.verificationRequest.CANCEL_BUTTON).click(); + + cy.get(locators.verificationRequest.DETAIL_PUBLICATION_DATE).should("be.visible").and("not.contain", getPastDay(10).format("DD/MM/YYYY")); + cy.get(locators.verificationRequest.DETAIL_SOURCE_1).should("not.exist"); + }) + }); + + describe("Minimum verification request flow", () => { + let minRequestHash: string; + + it("should allow request creation using only the minimum mandatory information", () => { + openCreateVerificationRequestForm(); + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + + cy.get(locators.verificationRequest.FORM_CONTENT).type(minimumContent); + selectPublicationDate(today); + saveVerificationRequest(); + cy.wait("@getVerification").then((interception) => { + getHashFromUrl(interception) + minRequestHash = getHashFromUrl(interception); + + cy.log("Hash capturado:", minRequestHash); + }); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_CONTENT, minimumContent], + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, today.format("DD/MM/YYYY")], + ]) + }); + + it("should supplement a minimalist request by adding its first source during edition", () => { + goToVerificationRequest(minRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_0).type(`https://${fullVerificationRequest.source}`); + selectPublicationDate(getPastDay(1)); + saveVerificationRequest(); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, getPastDay(1).format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_0, fullVerificationRequest.source], + ]) + }) + }) + }); +}); diff --git a/cypress/fixtures/verificationRequest.ts b/cypress/fixtures/verificationRequest.ts new file mode 100644 index 000000000..83e9e3470 --- /dev/null +++ b/cypress/fixtures/verificationRequest.ts @@ -0,0 +1,16 @@ +const fullVerificationRequest = { + content: "Verification Request Content", + reportType: "Discurso", + impactArea: "Ambientalismo", + topic: "Socialismo", + heardFrom: "Verification Request heardFrom", + source: "wikimedia.org", + email: "test-cypress@aletheiafact.org", +}; + +const minimumContent = "Verification Request Content minimium" + +const regexVerificationRequestPage = /\/verification-request\/[\w-]+$/ +const updatedSource = "www.wikidata.org" + +export { fullVerificationRequest, regexVerificationRequestPage, updatedSource, minimumContent } diff --git a/cypress/support/locators.ts b/cypress/support/locators.ts index d71c356e1..3b0c82899 100644 --- a/cypress/support/locators.ts +++ b/cypress/support/locators.ts @@ -38,7 +38,6 @@ const locators = { BTN_SUBMIT_CLAIM: "[data-cy=testSaveButton]", INPUT_TITLE: "[data-cy=testTitleClaimForm]", INPUT_DATA: "[data-cy=testSelectDate]", - INPUT_DATA_TODAY: ".MuiPickersDay-today", INPUT_SOURCE: "[data-cy=testSource1]", }, @@ -52,6 +51,7 @@ const locators = { ADD_CLAIM: "[data-cy=testFloatButtonAddClaim]", ADD_PERSONALITY: "[data-cy=testFloatButtonAddPersonality]", ADD_SOURCE: "[data-cy=testFloatButtonAddSources]", + ADD_VERIFICATION_REQUEST: "[data-cy=testFloatButtonAddVerificationRequest]" }, claimReview: { @@ -83,6 +83,49 @@ const locators = { "[data-cy=testClaimReviewSourcesButton]", }, + verificationRequest: { + FORM_CONTENT: "[data-cy=testClaimReviewcontent]", + FORM_REPORT_TYPE: "[data-cy=testClaimReviewreportType]", + FORM_IMPACT_AREA: "[data-cy=testClaimReviewimpactArea]", + FORM_HEARD_FROM: "[data-cy=testClaimReviewheardFrom]", + FORM_PUBLICATION_DATE: "[data-cy=testSelectDate]", + FORM_SOURCE: "[data-cy=testClaimReviewsource]", + FORM_EMAIL: "[data-cy=testClaimReviewemail]", + + FORM_SOURCE_ITEM_0: "[data-cy=testClaimReviewsourceEdit-0]", + FORM_SOURCE_ITEM_1: "[data-cy=testClaimReviewsourceEdit-1]", + FORM_SOURCE_ADD: "[data-cy=testClaimReviewsource-addSources]", + FORM_SOURCE_REMOVE_2: "[data-cy=testClaimReviewsourceRemove-2]", + + DETAIL_CONTENT: "[data-cy=testVerificationRequestContent]", + DETAIL_REPORT_TYPE: "[data-cy=testVerificationRequestReportType]", + DETAIL_IMPACT_AREA: "[data-cy=testVerificationRequestImpactArea]", + DETAIL_SOURCE_CHANNEL: "[data-cy=testVerificationRequestSourceChannel]", + DETAIL_SEVERITY: "[data-cy=testVerificationRequestSeverity]", + DETAIL_HEARD_FROM: "[data-cy=testVerificationRequestHeardFrom]", + DETAIL_PUBLICATION_DATE: "[data-cy=testVerificationRequestPublicationDate]", + DETAIL_DATE: "[data-cy=testVerificationRequestDate]", + DETAIL_SOURCE_0: "[data-cy=testVerificationRequestSource0]", + DETAIL_SOURCE_1: "[data-cy=testVerificationRequestSource1]", + + ERROR_VALIDATION_CONTENT: "[data-cy=testClaimReviewErrorcontent]", + ERROR_VALIDATION_PUBLICATION_DATE: "[data-cy=testClaimReviewErrorpublicationDate]", + + DETAIL_CARD_CONTENT: "[data-cy=testVerificationRequestCardContent0]", + DETAIL_CARD_CONTENT_1: "[data-cy=testVerificationRequestCardContent1]", + + ADD_TOPIC_ICON: "[data-cy=testVerificationRequestTopicsToggle]", + TYPE_TOPIC_INPUT: "[data-cy=testVerificationRequestTopicsInput]", + ADD_TOPIC_SUBMIT: "[data-cy=testVerificationRequestAddTopicButton]", + DETAIL_TOPIC_TAG: "[data-cy=testVerificationRequestTopicChip0]", + REMOVE_TOPIC_ICON: "[data-cy=testVerificationRequestTopicRemoveButton0]", + + SEE_FULL_BUTTON: "[data-cy=testSeeFullVerificationRequest]", + EDIT_BUTTON: "[data-cy=testVerificationRequestEditButton]", + SAVE_BUTTON: "[data-cy=testSaveButton]", + CANCEL_BUTTON: "[data-cy=testCancelButton]", + }, + menu: { SIDE_MENU: "[data-cy=testOpenSideMenu]", USER_ICON: "[data-cy=testUserIcon]", diff --git a/cypress/utils/dateUtils.ts b/cypress/utils/dateUtils.ts new file mode 100644 index 000000000..96bb020d4 --- /dev/null +++ b/cypress/utils/dateUtils.ts @@ -0,0 +1,6 @@ +import dayjs from "dayjs"; + +export const today = dayjs(); + +export const getPastDay = (daysAgo: number) => + dayjs().subtract(daysAgo, "day"); diff --git a/src/components/AletheiaMenu.tsx b/src/components/AletheiaMenu.tsx index ac9d94a66..52cd63765 100644 --- a/src/components/AletheiaMenu.tsx +++ b/src/components/AletheiaMenu.tsx @@ -12,6 +12,7 @@ import { Roles } from "../types/enums"; import { NameSpaceEnum } from "../types/Namespace"; import { currentNameSpace } from "../atoms/namespace"; import localConfig from "../../config/localConfig"; +import { isAdmin } from "../utils/GetUserPermission"; const AletheiaMenu = () => { const { t } = useTranslation(); @@ -99,7 +100,7 @@ const AletheiaMenu = () => { {t("menu:kanbanItem")} )} - {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( <> { const dispatch = useDispatch(); @@ -44,7 +45,7 @@ const ClaimView = ({ personality, claim, href, hideDescriptions }) => { return ( <> - {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( { diff --git a/src/components/ClaimReview/ClaimReviewView.tsx b/src/components/ClaimReview/ClaimReviewView.tsx index c33f194b0..b1021f62b 100644 --- a/src/components/ClaimReview/ClaimReviewView.tsx +++ b/src/components/ClaimReview/ClaimReviewView.tsx @@ -19,6 +19,7 @@ import { useAppSelector } from "../../store/store"; import { ReviewTaskStates } from "../../machines/reviewTask/enums"; import { generateReviewContentPath } from "../../utils/GetReviewContentHref"; import SentenceReportPreviewView from "../SentenceReport/SentenceReportPreviewView"; +import { isAdmin } from "../../utils/GetUserPermission"; export interface ClaimReviewViewProps { content: Content; @@ -80,7 +81,7 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => { return (
- {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( <> {review?.isPublished ? ( { )} {(isClaimTypeAndNotSmallScreen || isSourceOrVerificationRequest) && ( - - )} + + )} {enableViewReportPreview ? ( { const { t } = useTranslation(); @@ -31,7 +32,7 @@ const ReviewAlert = ({ isHidden, isPublished, hideDescription }) => { reviewNotStartedSelector ); const reviewData = useSelector(machineService, reviewDataSelector); - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); const isCrossChecking = useSelector(machineService, crossCheckingSelector); const isAddCommentCrossChecking = useSelector( machineService, diff --git a/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx b/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx index 121fe8226..54e2067f8 100644 --- a/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx +++ b/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx @@ -29,6 +29,7 @@ import WarningModal from "../../Modal/WarningModal"; import { currentNameSpace } from "../../../atoms/namespace"; import { CommentEnum, Roles } from "../../../types/enums"; import useAutoSaveDraft from "./hooks/useAutoSaveDraft"; +import { isAdmin } from "../../../utils/GetUserPermission"; const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const { @@ -123,9 +124,9 @@ const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const isValidReviewer = event === ReviewTaskEvents.sendToCrossChecking ? !data.crossCheckerId || - !reviewData.usersId.includes(data.crossCheckerId) + !reviewData.usersId.includes(data.crossCheckerId) : !data.reviewerId || - !reviewData.usersId.includes(data.reviewerId); + !reviewData.usersId.includes(data.reviewerId); setReviewerError(!isValidReviewer); return isValidReviewer; @@ -202,7 +203,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const userIsReviewer = reviewData.reviewerId === userId; const userIsCrossChecker = reviewData.crossCheckerId === userId; const userIsAssignee = reviewData.usersId.includes(userId); - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); if ( isReported && diff --git a/src/components/Debate/DebateHeader.tsx b/src/components/Debate/DebateHeader.tsx index 87ddd4d7e..8ce9e3707 100644 --- a/src/components/Debate/DebateHeader.tsx +++ b/src/components/Debate/DebateHeader.tsx @@ -12,7 +12,7 @@ import { NameSpaceEnum } from "../../types/Namespace"; import { currentNameSpace } from "../../atoms/namespace"; import AletheiaButton, { ButtonType } from "../Button"; import { EditOutlined } from "@mui/icons-material"; -import { Roles } from "../../types/enums"; +import { isAdmin } from "../../utils/GetUserPermission"; const DebateHeader = ({ claim, title, personalities, userRole }) => { const [personalitiesArray, setPersonalitiesArray] = useState(personalities); @@ -49,7 +49,7 @@ const DebateHeader = ({ claim, title, personalities, userRole }) => { style={{ paddingTop: "32px", backgroundColor: colors.lightNeutral, - justifyContent:"center" + justifyContent: "center" }} >
{ {title}
- {userRole === Roles.Admin && claim?.claimId ? ( + {isAdmin(userRole) && claim?.claimId ? ( { marginRight: vw?.lg && vw?.md && vw?.sm ? 0 : 160, }} > - {t("debates:openEditDebateMode")} + {t("debates:openEditDebateMode")} ) : null} { setOpen(true)} - data-cy={props} + data-cy={props.dataCy} {...props} />} PopperProps={{ placement: 'bottom-start', }} diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 55de5a8a6..9a84795b3 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -75,7 +75,11 @@ const DynamicForm = ({ )} /> {errors[fieldName] && ( - + {t(errors[fieldName].message)} )} diff --git a/src/components/Form/DynamicInput.tsx b/src/components/Form/DynamicInput.tsx index b8bdd5e2b..9596e57b4 100644 --- a/src/components/Form/DynamicInput.tsx +++ b/src/components/Form/DynamicInput.tsx @@ -92,10 +92,11 @@ const DynamicInput = (props: DynamicInputProps) => { case "sourceList": return ( props.onChange(value)} disabled={props.disabled} placeholder={props.placeholder} + dataCy={props["data-cy"]} /> ); case "select": @@ -114,6 +115,7 @@ const DynamicInput = (props: DynamicInputProps) => { onChange={(value) => props.onChange(value)} placeholder={t(props.placeholder)} isDisabled={props.disabled} + dataCy={props["data-cy"]} /> ); case "selectImpactArea": @@ -123,6 +125,7 @@ const DynamicInput = (props: DynamicInputProps) => { onChange={(value) => props.onChange(value)} placeholder={t(props.placeholder)} isDisabled={props.disabled} + dataCy={props["data-cy"]} /> ); case "imageUpload": @@ -168,7 +171,7 @@ const DynamicInput = (props: DynamicInputProps) => { defaultValue={props.defaultValue} placeholder={t(props.placeholder)} onChange={(value) => props.onChange(value)} - data-cy={"testSelectDate"} + data-cy="testSelectDate" disabledDate={props.disabledDate} disabled={props.disabled} style={{ backgroundColor: props.disabled ? colors.lightNeutral : colors.white }} diff --git a/src/components/SentenceReport/SentenceReportPreviewView.tsx b/src/components/SentenceReport/SentenceReportPreviewView.tsx index 78a30a73e..d31b851e0 100644 --- a/src/components/SentenceReport/SentenceReportPreviewView.tsx +++ b/src/components/SentenceReport/SentenceReportPreviewView.tsx @@ -21,6 +21,7 @@ import SentenceReportPreview from "./SentenceReportPreview"; import { useAppSelector } from "../../store/store"; import ClaimReviewHeader from "../ClaimReview/ClaimReviewHeader"; import { Content } from "../../types/Content"; +import { isAdmin } from "../../utils/GetUserPermission"; interface ISentenceReportViewV1Props { context: any; @@ -73,7 +74,7 @@ const SentenceReportPreviewView = ({ const isPublished = useSelector(machineService, publishedSelector) || publishedReview?.review; - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); const userIsCrossChecker = context.crossCheckerId === userId; const userIsAssignee = context.usersId.includes(userId); diff --git a/src/components/SentenceReport/SentenceReportView.tsx b/src/components/SentenceReport/SentenceReportView.tsx index b4361f6c0..0cd90c943 100644 --- a/src/components/SentenceReport/SentenceReportView.tsx +++ b/src/components/SentenceReport/SentenceReportView.tsx @@ -3,7 +3,6 @@ import { Grid } from "@mui/material"; import React, { useContext } from "react"; import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider"; -import { Roles } from "../../types/enums"; import { reviewingSelector, publishedSelector, @@ -18,6 +17,7 @@ import { useAtom } from "jotai"; import { currentUserId, currentUserRole } from "../../atoms/currentUser"; import SentenceReportComments from "./SentenceReportComments"; import { ReviewTaskTypeEnum } from "../../../server/types/enums"; +import { isAdmin } from "../../utils/GetUserPermission"; const SentenceReportView = ({ context, @@ -44,7 +44,7 @@ const SentenceReportView = ({ const isPublished = useSelector(machineService, publishedSelector) || publishedReview?.review; - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); const canShowClassificationAndCrossChecking = ((isCrossChecking || isAddCommentCrossChecking) && diff --git a/src/components/SharedFormFooter.tsx b/src/components/SharedFormFooter.tsx index 98d51a517..226986fde 100644 --- a/src/components/SharedFormFooter.tsx +++ b/src/components/SharedFormFooter.tsx @@ -11,7 +11,8 @@ interface ISharedFormFooter { isLoading: boolean; setRecaptchaString: Dispatch>; hasCaptcha: boolean; - hasCancelButton?: boolean; + isDrawerOpen?: boolean; + onClose?: () => void; extraButton?: React.ReactNode; } @@ -19,7 +20,8 @@ const SharedFormFooter = ({ isLoading, setRecaptchaString, hasCaptcha, - hasCancelButton = true, + isDrawerOpen, + onClose, extraButton }: ISharedFormFooter) => { const recaptchaRef = useRef(null); @@ -35,15 +37,16 @@ const SharedFormFooter = ({ justifyContent: "space-evenly", }} > - {hasCancelButton ? - router.back()} - > - {t("claimForm:cancelButton")} - - : extraButton - } + isDrawerOpen ? onClose() : router.back()} + data-cy="testCancelButton" + > + {t("claimForm:cancelButton")} + + + {extraButton} + = ({ icon, label, label_value, style }) => { +export const MetaChip: React.FC = ({ icon, label, label_value, style, dataCy }) => { const { t } = useTranslation(); return ( = ({ icon, label, label_value, st label={label_value || t("claimForm:noAnswer")} style={style} size="small" + data-cy={dataCy} /> ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/RequestDates.tsx b/src/components/VerificationRequest/RequestDates.tsx index c8b808838..d35a92bc7 100644 --- a/src/components/VerificationRequest/RequestDates.tsx +++ b/src/components/VerificationRequest/RequestDates.tsx @@ -6,18 +6,21 @@ interface RequestDatesProps { icon?: React.ReactNode; label: string; value: string; + dataCy?: string; } -export const RequestDates: React.FC = ({ icon, label, value }) => { +export const RequestDates: React.FC = ({ icon, label, value, dataCy }) => { const publicationDate = new Date(value); const isValidDate = !Number.isNaN(publicationDate.getTime()); return ( - + {icon} {label} @@ -25,4 +28,4 @@ export const RequestDates: React.FC = ({ icon, label, value } ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/SourceListVerificationRequest.tsx b/src/components/VerificationRequest/SourceListVerificationRequest.tsx index d27e1a31f..1f6a93914 100644 --- a/src/components/VerificationRequest/SourceListVerificationRequest.tsx +++ b/src/components/VerificationRequest/SourceListVerificationRequest.tsx @@ -21,6 +21,7 @@ const SourceList: React.FC = ({ sources, t, id }) => { {flatSources.map((source, index) => ( { - {groupedRequests[status.key].map((request) => ( + {groupedRequests[status.key].map((request, index) => ( { diff --git a/src/components/VerificationRequest/VerificationRequestCard.tsx b/src/components/VerificationRequest/VerificationRequestCard.tsx index 833a6f9bf..f5b7d3bbd 100644 --- a/src/components/VerificationRequest/VerificationRequestCard.tsx +++ b/src/components/VerificationRequest/VerificationRequestCard.tsx @@ -68,6 +68,7 @@ const VerificationRequestCard = ({ label_value: t( `claimForm:${verificationRequest.reportType || "undefined"}` ), + dataCy: "testVerificationRequestReportType", style: { backgroundColor: colors.secondary, color: colors.white }, }, { @@ -79,6 +80,7 @@ const VerificationRequestCard = ({ `verificationRequest:${verificationRequest.sourceChannel}`, { defaultValue: verificationRequest.sourceChannel }, ), + dataCy: "testVerificationRequestSourceChannel", style: { backgroundColor: colors.primary, color: colors.white }, }, { @@ -86,6 +88,7 @@ const VerificationRequestCard = ({ key: `${verificationRequest._id}|impactArea`, label: t("verificationRequest:tagImpactArea"), label_value: verificationRequest.impactArea?.name, + dataCy: "testVerificationRequestImpactArea", style: { backgroundColor: colors.neutralSecondary, color: colors.white, @@ -96,6 +99,7 @@ const VerificationRequestCard = ({ key: `${verificationRequest._id}|severity`, label: t("verificationRequest:tagSeverity"), label_value: getSeverityLabel(verificationRequest.severity, t), + dataCy: "testVerificationRequestSeverity", style: { backgroundColor: getSeverityColor(verificationRequest.severity), color: colors.white, @@ -132,6 +136,7 @@ const VerificationRequestCard = ({ {verificationRequest.publicationDate && ( } label={t("verificationRequest:tagPublicationDate")} value={verificationRequest.publicationDate} @@ -162,6 +168,7 @@ const VerificationRequestCard = ({ {verificationRequest.date && ( } label={t("verificationRequest:tagDate")} value={verificationRequest.date} @@ -173,6 +180,7 @@ const VerificationRequestCard = ({ )} @@ -188,7 +196,7 @@ const VerificationRequestCard = ({ {metaChipData.map( - ({ icon, key, label, label_value, style }) => ( + ({ icon, key, label, label_value, style, dataCy }) => ( ) diff --git a/src/components/VerificationRequest/VerificationRequestContent.tsx b/src/components/VerificationRequest/VerificationRequestContent.tsx index 09239789f..ab37969a9 100644 --- a/src/components/VerificationRequest/VerificationRequestContent.tsx +++ b/src/components/VerificationRequest/VerificationRequestContent.tsx @@ -4,15 +4,16 @@ import { Box, Typography } from "@mui/material"; interface VerificationRequestContentProps { label: string; value: React.ReactNode; + dataCy?: string; } -export const VerificationRequestContent: React.FC = ({ label, value }) => { +export const VerificationRequestContent: React.FC = ({ label, value, dataCy }) => { return ( - + {label} @@ -20,4 +21,4 @@ export const VerificationRequestContent: React.FC ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx b/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx index 0d81e09ca..d5d837c71 100644 --- a/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx +++ b/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx @@ -24,7 +24,7 @@ import { ReviewTaskEvents, ReviewTaskTypeEnum, } from "../../machines/reviewTask/enums"; -import { Roles, VerificationRequestStatus } from "../../types/enums"; +import { VerificationRequestStatus } from "../../types/enums"; import { currentUserRole } from "../../atoms/currentUser"; import { useAppSelector } from "../../store/store"; import colors from "../../styles/colors"; @@ -47,6 +47,7 @@ import { getSeverityColor, getSeverityLabel, } from "../../helpers/verificationRequestCardHelper"; +import { isStaff } from "../../utils/GetUserPermission"; interface VerificationRequestDetailDrawerProps { verificationRequest: any; @@ -83,11 +84,7 @@ const VerificationRequestDetailDrawer: React.FC { setCurrentRequest(verificationRequest); @@ -282,6 +279,7 @@ const VerificationRequestDetailDrawer: React.FC - {role == Roles.Admin && ( + {isAdmin(role) && ( setOpenEditDrawer(true)} /> @@ -64,7 +65,7 @@ const VerificationRequestMainContent = ({ {!vw.xs && - role !== Roles.Regular && + isStaff(role) && verificationRequestGroup?.length > 0 && ( { const { handleSubmit, @@ -40,6 +42,8 @@ const DynamicVerificationRequestForm = ({ isLoading={isLoading} setRecaptchaString={setRecaptchaString} hasCaptcha={hasCaptcha} + isDrawerOpen={isDrawerOpen} + onClose={onClose} /> ); diff --git a/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx b/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx index 70218611b..a66b3dcfb 100644 --- a/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx +++ b/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx @@ -16,12 +16,18 @@ const EditVerificationRequestDrawer = ({ const [recaptchaString, setRecaptchaString] = useState(""); const hasCaptcha = !!recaptchaString; const [isLoading, setIsLoading] = useState(false); + const sourceMapped = verificationRequest.source?.map(source => source.href); + + const updatedVerificationRequest = { + ...verificationRequest, + source: sourceMapped?.length > 0 ? sourceMapped : undefined + } const onSubmit = async (data) => { try { const updateData = { publicationDate: data.publicationDate, - source: data.source.map(url => ({ href: url })), + source: data.source?.map(url => ({ href: url })), }; const response = await verificationRequestApi.updateVerificationRequest( @@ -50,12 +56,14 @@ const EditVerificationRequestDrawer = ({ diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx index 7fe5a3d71..cfd5a2a07 100644 --- a/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx @@ -7,8 +7,9 @@ const ImpactAreaSelect = ({ onChange, placeholder, isDisabled, + dataCy, }: IImpactAreaSelect) => { - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -25,6 +26,7 @@ const ImpactAreaSelect = ({ setSelectedTags={setValue} isMultiple={false} isDisabled={isDisabled} + dataCy={dataCy} /> ); }; diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx index d26b2aaca..5e6e844cd 100644 --- a/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Grid } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -7,13 +7,14 @@ import AletheiaButton, { ButtonType } from "../../../Button"; import AletheiaInput from "../../../AletheiaInput"; import { IInputExtraSourcesList } from "../../../../types/VerificationRequest"; import { SourceType } from "../../../../types/Source"; +import { debounce } from "lodash"; const formatSources = (sources: SourceType[]) => { const sourceArray = Array.isArray(sources) ? sources : []; if (sourceArray.length === 0) return [createEmptySource()]; return sourceArray.map((source) => ({ - id: source._id, + id: Math.random().toString(), href: typeof source === "object" ? source.href : source || "", isNewSource: !!(typeof source === "object" ? source.href : source), })); @@ -25,34 +26,43 @@ const createEmptySource = () => ({ isNewSource: false }); -const InputExtraSourcesList = ({ sources, onChange, disabled, placeholder }: IInputExtraSourcesList) => { +const InputExtraSourcesList = ({ defaultSources, onChange, disabled, placeholder, dataCy }: IInputExtraSourcesList) => { const { t } = useTranslation(); - const [sourcesList, setSourcesList] = useState(() => formatSources(sources as SourceType[])); + const [sourcesList, setSourcesList] = useState(() => formatSources(defaultSources as SourceType[])); - const handleSubmit = (hrefsList: typeof sourcesList) => { - const updatedHrefs = [...new Set(hrefsList.map(source => source.href.trim()).filter(Boolean))]; - onChange(updatedHrefs); - }; + const handleListChange = useCallback((newSourcesList: typeof sourcesList) => { + const cleanedSources = [...new Set(newSourcesList.map(source => source.href.trim()).filter(Boolean))]; + onChange(cleanedSources); + }, [onChange]); + + const debouncedOnChange = useMemo( + () => + debounce((SourcesList: typeof sourcesList) => { + handleListChange(SourcesList) + }, 800), + [handleListChange] + ); const updateSources = (id: string, newHref: string) => { - const newHrefList = sourcesList.map(source => source.id === id ? { ...source, href: newHref } : source); - setSourcesList(newHrefList); - handleSubmit(newHrefList); + const newSourcesList = sourcesList.map(source => source.id === id ? { ...source, href: newHref } : source); + setSourcesList(newSourcesList); + debouncedOnChange(newSourcesList) }; const addField = () => { if (disabled) return; - const newHrefList = [...sourcesList, createEmptySource()]; - setSourcesList(newHrefList); - handleSubmit(newHrefList); + const newSourcesList = [...sourcesList, createEmptySource()]; + setSourcesList(newSourcesList); }; const removeField = (id: string, index: number) => { if (disabled || index === 0) return; - const newHrefList = sourcesList.filter((source) => source.id !== id); + const newSourcesList = sourcesList.filter((source) => source.id !== id); + + setSourcesList(newSourcesList); + debouncedOnChange.cancel(); - setSourcesList(newHrefList); - handleSubmit(newHrefList); + handleListChange(newSourcesList) }; return ( @@ -72,12 +82,14 @@ const InputExtraSourcesList = ({ sources, onChange, disabled, placeholder }: IIn disabled={disabled || (index === 0 && source.isNewSource)} onChange={(newHref) => updateSources(source.id, newHref.target.value)} placeholder={t(placeholder)} + data-cy={`${dataCy}Edit-${index}`} white="true" /> {!disabled && index !== 0 && ( removeField(source.id, index)} + data-cy={`${dataCy}Remove-${index}`} style={{ minWidth: "40px" }} > @@ -91,6 +103,7 @@ const InputExtraSourcesList = ({ sources, onChange, disabled, placeholder }: IIn diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx index 6bccd5df8..30110105a 100644 --- a/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx @@ -12,6 +12,7 @@ const ReportTypeSelect = ({ placeholder, style = {}, isDisabled, + dataCy, }: IReportTypeSelect) => { const [value, setValue] = useState(defaultValue || ""); const { t } = useTranslation(); @@ -32,6 +33,7 @@ const ReportTypeSelect = ({ value={value} style={style} disabled={isDisabled} + data-cy={dataCy} > {placeholder} diff --git a/src/components/badges/BadgesFormDrawer.tsx b/src/components/badges/BadgesFormDrawer.tsx index 5007cfe7a..c3c0ba0d2 100644 --- a/src/components/badges/BadgesFormDrawer.tsx +++ b/src/components/badges/BadgesFormDrawer.tsx @@ -126,6 +126,8 @@ const BadgesFormDrawer = () => { badges={updatedBadges} onSubmit={onSubmit} isLoading={isLoading} + isDrawerOpen={open} + onClose={onCloseDrawer} /> diff --git a/src/components/badges/DynamicBadgesForm.tsx b/src/components/badges/DynamicBadgesForm.tsx index 9977ff5d0..1a22d08c5 100644 --- a/src/components/badges/DynamicBadgesForm.tsx +++ b/src/components/badges/DynamicBadgesForm.tsx @@ -11,6 +11,8 @@ const DynamicBadgesForm = ({ badges, onSubmit, isLoading, + isDrawerOpen, + onClose }: IDynamicBadgesForm) => { const { handleSubmit, @@ -37,7 +39,8 @@ const DynamicBadgesForm = ({ isLoading={isLoading} setRecaptchaString={setRecaptchaString} hasCaptcha={hasCaptcha} - hasCancelButton={false} + isDrawerOpen={isDrawerOpen} + onClose={onClose} /> diff --git a/src/components/namespace/DynamicNameSpaceForm.tsx b/src/components/namespace/DynamicNameSpaceForm.tsx index 09911d44f..c576cf3da 100644 --- a/src/components/namespace/DynamicNameSpaceForm.tsx +++ b/src/components/namespace/DynamicNameSpaceForm.tsx @@ -14,6 +14,8 @@ const DynamicNameSpaceForm = ({ onSubmit, isLoading, setIsLoading, + isDrawerOpen, + onClose, t, }: IDynamicNameSpaceForm) => { const { @@ -56,7 +58,8 @@ const DynamicNameSpaceForm = ({ isLoading={isLoading} setRecaptchaString={setRecaptchaString} hasCaptcha={hasCaptcha} - hasCancelButton={false} + isDrawerOpen={isDrawerOpen} + onClose={onClose} extraButton={nameSpace?._id && (